Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all 525 articles
Browse latest View live

AEM CQ 56 - Add Scheduled Activations to Activate Later

$
0
0

Goal


In this post we are going to add the necessary logic to show scheduled activations of page or group of pages in the Activate Later dialog of siteadmin console (http://localhost:4502/siteadmin). Package is available for install,Source code and demo video are available for download

Activate Later



Customized Activate Later




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

Solution


1) There are two parts. First, code servlet to return the scheduled activations of page.

2) Add Scheduled Activations grid to the siteadmin console Activate Later dialog.

Create Servlet


1) The first step is creating a servlet and registering it as an OSGI component in CQ. Read this post on how to create an OSGI component for deploying to CQ.

2) Create servlet ScheduledActivationInstances and add the following code

package apps.mysample.scheduledactivations;

import com.day.cq.workflow.WorkflowService;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.Workflow;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.day.cq.workflow.ui.JcrPathBuilderManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferencePolicy;
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.resource.ValueMap;
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.text.SimpleDateFormat;
import java.util.*;

@SlingServlet(
paths="/bin/mycomponents/schactivation",
methods = "GET",
metatype = false,
label = "Scheduled Activation Instances"
)
public class ScheduledActivationInstances extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(ScheduledActivationInstances.class);
private static SimpleDateFormat FORMATTER = new SimpleDateFormat("EEE, dd MMM, yyyy HH:mm");

private static final String ACTIVATE_MODEL = "/etc/workflow/models/scheduled_activation/jcr:content/model";
private static final String DEACTIVATE_MODEL = "/etc/workflow/models/scheduled_deactivation/jcr:content/model";

@Reference(policy = ReferencePolicy.STATIC)
private WorkflowService wfService;

@Reference(policy = ReferencePolicy.STATIC)
JcrPathBuilderManager pathBuilder;

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

JSONWriter jw = new JSONWriter(response.getWriter());
String pageStr = request.getParameter("path");

try{
jw.object();

if(StringUtils.isEmpty(pageStr)){
jw.key("error").value("page input required");
jw.endObject();
return;
}

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

if(StringUtils.isEmpty(type)){
type = "ACTIVATE";
}

pageStr = pageStr.trim();
List<String> pages = Arrays.asList(pageStr.split(","));

ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
WorkflowSession wfSession = wfService.getWorkflowSession(session);

String[] state = new String[] { "RUNNING" };
Workflow workflows[] = wfSession.getWorkflows(state);

WorkflowData data = null;
String payload, absTime = null;

MetaDataMap mdMap = null;
Map<String, List<Map<String, String>>> retMap = new HashMap<String, List<Map<String, String>>>();

Map<String, String> map = null;
List<Map<String, String>> list = null;
Resource resource = null;

for(Workflow w : workflows){
data = w.getWorkflowData();

if(type.equals("ACTIVATE") && !w.getWorkflowModel().getId().equals(ACTIVATE_MODEL)){
continue;
}else if(type.equals("DEACTIVATE") && !w.getWorkflowModel().getId().equals(DEACTIVATE_MODEL)){
continue;
}

if (!"JCR_PATH".equals(data.getPayloadType()) && !"URL".equals(data.getPayloadType())) {
continue;
}

if (data.getPayloadType().equals("JCR_PATH") && w.getWorkItems().size() > 0) {
payload = pathBuilder.getPath(w.getWorkItems().get(0));
} else {
payload = (String) data.getPayload();
}

if(StringUtils.isEmpty(payload)){
continue;
}

payload = payload.trim();

if(payload.startsWith("/cf#")){
payload = payload.substring(4);
}

if(payload.endsWith(".html")){
payload = payload.substring(0, payload.lastIndexOf(".html"));
}

if(!pages.contains(payload)){
continue;
}

mdMap = w.getMetaDataMap();

if(mdMap == null){
continue;
}

absTime = mdMap.get("absoluteTime", String.class);

if(StringUtils.isEmpty(absTime)){
continue;
}

list = retMap.get(payload);

if(list == null){
list = new ArrayList<Map<String, String>>();
retMap.put(payload, list);
}

map = new HashMap<String, String>();
list.add(map);

resource = resolver.getResource(payload);

if(resource != null){
map.put("id", w.getId());
map.put("name", resource.getChild("jcr:content").adaptTo(ValueMap.class).get("jcr:title", String.class));
map.put("st",FORMATTER.format(w.getTimeStarted().getTime()));
map.put("ini", w.getInitiator());
map.put("dt", FORMATTER.format(Long.parseLong(absTime)));
}
}

String path = null;
Iterator<Map<String, String>> itr = null;

jw.key("data").array();

for(Map.Entry<String, List<Map<String, String>>> entry : retMap.entrySet()){
list = entry.getValue();
path = entry.getKey();

itr = list.iterator();

while(itr.hasNext()){
jw.object();
jw.key("path").value(path);

for(Map.Entry<String, String> mEntry : itr.next().entrySet()){
jw.key(mEntry.getKey()).value(mEntry.getValue());
}

jw.endObject();
}
}

jw.endArray();
jw.endObject();
}catch(Exception e){
log.error("Error getting schedule activation instances",e);
throw new ServletException(e);
}
}
}

3) Deploy the servlet and check if its working by accessing url http://localhost:4502/bin/mycomponents/schactivation?path=/content/geometrixx/es. If the page /content/geometrixx/es has scheduled activations set, you should see the following response

{"data":[{"path":"/content/geometrixx/es","dt":"Fri, 15 Nov, 2013 12:24","id":"/etc/workflow/instances/2013-11-08/model_12548425989383","name":"Español","st":"Fri, 08 Nov, 2013 12:24","ini":"admin"},{"path":"/content/geometrixx/es","dt":"Sat, 07 Dec, 2013 16:17","id":"/etc/workflow/instances/2013-11-08/model_26521845578422","name":"Español","st":"Fri, 08 Nov, 2013 16:17","ini":"admin"}]}


Extend SiteAdmin UI


1) The next step is extending siteadmin console UI and add Activations grid to Activate Later dialog

2) Use the overlay architecture of CQ to overlay /libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js. Create file /apps/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SiteAdmin = {
SA_GRID: "cq-siteadmin-grid",
ACTIVATE_BUT: "Activate",
ACTIVATE_LATER_BUT: "Activate Later...",
ACTIVATE_LATER_DID: "cq-activate-later-dialog",
ACTIVATIONS_GRID_ID_PREFIX: "myclientlib-scheduled-activations-grid",

getGrid: function(config){
var store = new CQ.Ext.data.Store({
baseParams: config.storeBaseParams,
proxy: new CQ.Ext.data.HttpProxy({
"autoLoad":false,
url: "/bin/mycomponents/schactivation",
method: 'GET'
}),
reader: new CQ.Ext.data.JsonReader({
root: 'data',
fields: [
{name: 'id', mapping: 'id'},
{name: 'name', mapping: 'name'},
{name: 'path', mapping: 'path'},
{name: 'actDate', mapping: 'dt'},
{name: 'startDate', mapping: 'st'},
{name: 'initiator', mapping: 'ini'}
]
})
});

store.load();

return new CQ.Ext.grid.GridPanel({
store: store,
id: config.gridId,
colModel: new CQ.Ext.grid.ColumnModel({
defaults: {
width: 120,
sortable: true
},
columns: [
{id: 'name', header: 'Name', width: 80, dataIndex: 'name'},
{id: 'path', header: 'Path', width: 160, dataIndex: 'path'},
{id: 'actDate', width: 160, header: config.actColHeader, dataIndex: 'actDate'},
{id: 'startDate', header: 'Start Date', dataIndex: 'startDate'},
{id: 'initiator', header: 'Initiator', width: 80, dataIndex: 'initiator'}
]
}),
sm: new CQ.Ext.grid.RowSelectionModel(),
tbar: [{
xtype: "tbbutton",
text: 'Terminate',
disabled: true,
tooltip: 'Terminate the selected workflows',
handler: function(){
var commentBox = new CQ.Ext.form.TextArea({
xtype: 'textarea',
name:'terminateComment',
fieldLabel:CQ.I18n.getMessage('Comment')
});

var tConfig = {
xtype: 'dialog',
title:CQ.I18n.getMessage('Terminate Workflow'),
params: {"_charset_":"utf-8"},
items: {
xtype:'panel',
items:[commentBox,{
xtype: 'hidden',
name:'state',
value:'ABORTED'
}]
},
buttons:[{
"text": CQ.I18n.getMessage("OK"),
"handler": function() {
var sGrid = CQ.Ext.getCmp(config.gridId);

var sFunc = function(options, success, response) {
if (!success) {
CQ.Ext.Msg.alert(CQ.I18n.getMessage("Error"),
CQ.I18n.getMessage("Termination of workflow failed"));
}else{
sGrid.getStore().reload();
}
};

CQ.Ext.each(sGrid.getSelectionModel().getSelections(), function(selection){
CQ.HTTP.post(selection.id,sFunc,{
"state":"ABORTED",
"_charset_":"utf-8",
"terminateComment": commentBox.getValue()
}
);
});

this.close();
}
},CQ.Dialog.CANCEL ]
};

var tDialog = CQ.WCM.getDialog(tConfig);
tDialog.show();
}
}],
width: 600,
height: 350,
frame: true,
title: config.title,
style: "margin:25px 0 0 0",
listeners: {
'click': function(){
var button = this.getTopToolbar().find('xtype','tbbutton')[0];
button.setDisabled(false);
}
}
});
},

addGrid: function(grid, config){
var toolBar = grid.getTopToolbar();

var actBut = toolBar.find("text", config.topButtonName)[0];
var actLaterBut = actBut.menu.find("text", config.laterButtonName);

if(!actLaterBut || actLaterBut.length == 0){
return;
}

actLaterBut[0].on('click', function(){
var nextId = CQ.Util.createId(config.dialogIdPrefix);
nextId = parseInt(nextId.substring(nextId.lastIndexOf("-") + 1), 10);

var prevId = config.dialogIdPrefix + "-" + (nextId - 1);
var dialog = CQ.Ext.getCmp(prevId);

if(!dialog){
return;
}

dialog.setWidth(700);
dialog.setHeight(500);

var panel = dialog.findBy(function(comp){
return comp["jcr:primaryType"] == "cq:Panel";
}, dialog);

if(!panel || panel.length == 0){
return;
}

panel = panel[0];
var paths = "";

CQ.Ext.each(grid.getSelectionModel().getSelections(), function(row){
paths = paths+ row.id + ",";
});

var gConfig = {};

gConfig.gridId = CQ.Util.createId(config.gridIdPrefix);
gConfig.title = config.gridTitle;
gConfig.actColHeader = config.actColHeader;
gConfig.storeBaseParams = { path : paths.substr(0, paths.lastIndexOf(",")), type: config.gridType };

panel.add(this.getGrid(gConfig));
panel.doLayout();
},this);
},

addScheduledActivationGrid: function(grid){
var config = {};

config.topButtonName = this.ACTIVATE_BUT;
config.laterButtonName = this.ACTIVATE_LATER_BUT;
config.dialogIdPrefix = this.ACTIVATE_LATER_DID;
config.gridIdPrefix = this.ACTIVATIONS_GRID_ID_PREFIX;
config.gridTitle = 'Pending Scheduled Activations';
config.actColHeader = 'Activation Date';
config.gridType = "ACTIVATE";

this.addGrid(grid, config);
}
};

$.getScript("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js", function(){
if(window.location.pathname == "/siteadmin"){
var INTERVAL = setInterval(function(){
var s = MyClientLib.SiteAdmin;
var grid = CQ.Ext.getCmp(s.SA_GRID);

if(grid){
clearInterval(INTERVAL);
s.addScheduledActivationGrid(grid);
}
}, 250);
}
}).fail(function(jqxhr, settings, exception){
console.log("Error parsing /libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js");
console.log(exception);
});


3) Done







AEM CQ 56 - Disable Activate Deactivate of Grid Toolbar and Context Menu

$
0
0

Goal


Disable the Activate and Deactivate functionality for folders in SiteAdmin console. Package is available for install and demo video for download



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

Solution


Using the overlay architecture of CQ overlay SiteAdmin actions js file. Create file /apps/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js to overlay /libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SiteAdmin = {
SA_GRID: "cq-siteadmin-grid",
SA_TREE: "cq-siteadmin-tree",
ACTIVATE_BUT: "Activate",
DEACTIVATE_BUT: "Deactivate",

disableMenu: function(grid){
var menu = grid.contextMenu;

if(!menu || (menu.mouseOverAdded == true)){
return;
}

menu.mouseOverAdded = true;

var actActivate = menu.find("text", this.ACTIVATE_BUT);
var actDeactivate = menu.find("text", this.DEACTIVATE_BUT);

if(actActivate.length == 0 || actDeactivate.length == 0){
return;
}

var disable = this.disableFn(grid);

//when the menu was first created on user selecting a folder, show event was already fired
//so trick user by setting disabled class,
//later on menu mouseover the Activate,Deactivate menu items are actually disabled
if(disable === true){
actActivate[0].addClass("x-item-disabled");
actDeactivate[0].addClass("x-item-disabled");
}

var menuDisableFn = function(){
var disable = this.disableFn(grid);

CQ.Ext.each([actActivate[0], actDeactivate[0]], function(but){
but.setDisabled(disable);

if(disable === false){
but.removeClass("x-item-disabled");
}else{
but.addClass("x-item-disabled");
}
});
};

menu.on('mouseover', menuDisableFn, this);
menu.on('show', menuDisableFn, this);
},

disableFn: function(sGrid){
var disable = false;

CQ.Ext.each(sGrid.getSelectionModel().getSelections(), function(row){
if(!disable && (row.data["type"] == "sling:OrderedFolder")){
disable = true;
}
});

return disable;
},

disableFolderActivateDeactivate: function(grid){
var toolBar = grid.getTopToolbar();
var actBut = toolBar.find("text", this.ACTIVATE_BUT);
var deActBut = toolBar.find("text", this.DEACTIVATE_BUT);

var tree = CQ.Ext.getCmp(this.SA_TREE);

var toggleButtons = function(disable){
actBut[0].setDisabled(disable);
deActBut[0].setDisabled(disable);
};

grid.on('rowcontextmenu',function(grid, index, e){
this.disableMenu(grid);
toggleButtons(this.disableFn(grid));
}, this);

grid.on('rowclick',function(){
toggleButtons(this.disableFn(grid));
}, this);

tree.on('selectionchange', function(t, nodePath){
var node = t.getSelectionModel().getSelectedNode();
toggleButtons(node.attributes["type"] == "sling:OrderedFolder");
});
}
};

$.getScript("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js", function(){
if(window.location.pathname == "/siteadmin"){
var INTERVAL = setInterval(function(){
var s = MyClientLib.SiteAdmin;
var grid = CQ.Ext.getCmp(s.SA_GRID);

if(grid){
clearInterval(INTERVAL);
s.disableFolderActivateDeactivate(grid);
}
}, 250);
}
}).fail(function(jqxhr, settings, exception){
console.log("Error parsing /libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.Actions.js");
console.log(exception);
});


AEM CQ 56 - New (Tag Tree) Tab in Sidekick

$
0
0

Goal


This post is on adding a new tab to the Sidekick. Here we extend Sidekick and add a new tab showing CQ Tag Tree. To add new tags or remove tags for a page, multiple clicks are involved in opening Sidekick -> Page Tab -> Page Properties -> Basic -> Tags

If adding and removing tags is a common use-case in your project, adding the tag tree in a sidekick tab could be useful and saves some clicks. The new tab loads a checkbox tree; to add a tag check the box and to remove uncheck. If a tag is checked, parent tags are shown in bold to identify the tags checked, deep down in the tree. Source code, demo video andpackage installare available for download



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

Tags Servlet


First, we need a servlet (deployed as OSGI component ) to feed the Tag tree ( explained in next section ) with tags json data. Create servlet GetTagsCheckedForPage and add the following code

package apps.mysample.sidekick;

import com.day.cq.commons.LabeledResource;
import com.day.cq.tagging.JcrTagManagerFactory;
import com.day.cq.tagging.Tag;
import com.day.cq.tagging.TagManager;
import com.day.text.Text;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Reference;
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.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@SlingServlet (
paths="/bin/mycomponents/sidekick/tags",
methods = "GET",
metatype = true,
label = "Tags Servlet"
)
public class GetTagsCheckedForPage extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(GetTagsCheckedForPage.class);

@Reference
JcrTagManagerFactory tmf;

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

String pagePath = request.getParameter("page");
String tagPath = request.getParameter("tag");

try{
ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
TagManager tMgr = tmf.getTagManager(session);

JSONWriter jw = new JSONWriter(response.getWriter());

if(StringUtils.isEmpty(pagePath) || StringUtils.isEmpty(tagPath)){
jw.object();
jw.key("error").value("required parameters page and tag missing");
jw.endObject();
return;
}

Resource resource = resolver.getResource(pagePath + "/jcr:content");

if(resource == null){
jw.object();
jw.key("error").value("resource " + pagePath + " not found");
jw.endObject();
return;
}

Resource parentTag = resolver.getResource(tagPath);

if(parentTag == null){
jw.object();
jw.key("error").value("tag " + parentTag + " not found");
jw.endObject();
return;
}

Tag[] pageTags = tMgr.getTags(resource);
List<String> pageTagsList = new ArrayList<String>();

for(Tag t : pageTags){
pageTagsList.add(t.getPath());
}

Iterator<Resource> itr = parentTag.listChildren();

Resource tag = null;
Node node = null;
String parentPath = null, cls = null;

jw.array();

while(itr.hasNext()){
tag = itr.next();

if(!tag.getResourceType().equals("cq/tagging/components/tag")){
continue;
}

parentPath = tag.getParent().getPath();

jw.object();

jw.key("name").value(tag.getPath().substring(1));

cls = parentPath.equals("/etc/tags") || parentPath.equals("/etc") ? "folder" : "tag x-tree-node-icon";

for(Tag t : pageTags){
if(t.getPath().indexOf(tag.getPath()) == 0){
//Make the breadcrumb trail bold, the css class x-menu-item-default is defined in CQ as
//.x-menu-item-default SPAN { font-weight:bold; }
cls = "x-menu-item-default " + cls;
break;
}
}

jw.key("cls").value(cls);

node = tag.adaptTo(Node.class);

if(node.hasProperty("jcr:title")){
jw.key("text").value(node.getProperty("jcr:title").getString());
}else{
jw.key("text").value(node.getName());
}

jw.key("checked").value(pageTagsList.contains(tag.getPath()));

jw.endObject();
}

jw.endArray();
}catch(Exception e){
LOG.error("Error getting tags",e);
throw new ServletException(e);
}
}
}


UI Extension


1) Next step is adding a UI extension, for generating Tag tree and adding it in a new Sidekick tab.

2) Using overlay architecture of CQ, overlay the file /libs/cq/ui/widgets/source/widgets/wcm/ContentFinder.js by creating /apps/cq/ui/widgets/source/widgets/wcm/ContentFinder.js ( If not ContentFinder.js,  developer can opt overlaying Sidekick.js etc, any code added in the overlay file should be executed after Sidekick is loaded on the page )

3) In /apps/cq/ui/widgets/source/widgets/wcm/ContentFinder.js add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.Sidekick = {
SK_TAB_PANEL: "cq-sk-tabpanel",
TAGADMIN_TREE_ID: "myclientlib-cq-tagadmin-tree",
TAGS : "TAGS",

addTagsPanel: function(sk){
var CONTEXTS = CQ.wcm.Sidekick.CONTEXTS;

if( ($.inArray(this.TAGS, CONTEXTS) != -1) || sk.panels[this.TAGS]){
return;
}

CONTEXTS.push(this.TAGS);

var tabPanel = sk.findById(this.SK_TAB_PANEL);

var treeLoader = new CQ.Ext.tree.TreeLoader({
dataUrl:"/bin/mycomponents/sidekick/tags",
requestMethod: "GET",
baseParams: {
page: sk.getPath()
},
listeners: {
beforeload: function(tl, node){
this.baseParams.tag = "/" + node.attributes.name;
}
}
});

var treeRoot = new CQ.Ext.tree.AsyncTreeNode({
name: "etc/tags",
text: CQ.I18n.getMessage("Tags"),
expanded: true
});

var tree = new CQ.Ext.tree.TreePanel({
"id": this.TAGADMIN_TREE_ID,
"margins":"5 0 5 5",
"width": 200,
"animate":true,
"loader": treeLoader,
"root": treeRoot,
"rootVisible": false,
"tbar": [{
"iconCls":"cq-siteadmin-refresh",
"handler":function(){
CQ.Ext.getCmp(MyClientLib.Sidekick.TAGADMIN_TREE_ID).getRootNode().reload();
},
"tooltip": {
"text":CQ.I18n.getMessage("Refresh the tree")
}
}],
listeners: {
checkchange : function( cNode, checked ){
var tagTree = CQ.Ext.getCmp(MyClientLib.Sidekick.TAGADMIN_TREE_ID);
var tag = cNode.attributes.name;

//to create something like geometrixx-media:entertainment/music
tag = tag.substr("etc/tags".length + 1);
tag = tag.substr(0, tag.indexOf("/")) + ":" + tag.substr(tag.indexOf("/") + 1) ;

var data = { "./cq:tags@TypeHint" : "String[]", "./cq:tags@Patch" : "true",
"./cq:tags" : (checked ? "+" : "-") + tag };

$.ajax({
url: sk.getPath() + "/jcr:content",
dataType: "json",
data: data,
success: function(rdata){
var pNodes = [];
var pNode = cNode.parentNode;

while(true){
if(pNode.attributes.name == "etc/tags"){
break;
}

pNodes.push(pNode);
pNode = pNode.parentNode;
}

var dec = pNodes.length - 1;

var callBack = function(rNode){
if(dec < 0 ){
return;
}

var toRefresh;

CQ.Ext.each(rNode.childNodes, function(child){
if(!toRefresh && (child.attributes.name == pNodes[dec].attributes.name)){
toRefresh = child;
}
});

if(toRefresh){
dec--;
toRefresh.reload(callBack);
}
};

tagTree.getRootNode().reload(callBack);
},
type: 'POST'
});
}
}
});

sk.panels[this.TAGS] = new CQ.Ext.Panel({
"border": false,
//"autoScroll": true,
"layout": "fit",
items: [tree],
"id": "cq-sk-tab-" + this.TAGS
});

tabPanel.add({
"tabTip": "Tags",
"iconCls": "cq-sidekick-tab cq-cft-tab-icon full",
"items": sk.panels[this.TAGS],
"layout": "fit"
});

sk.doLayout();
}
};

if(window.location.pathname.indexOf("/cf") == 0 || window.location.pathname.indexOf("/content") == 0){
$.getScript("/libs/cq/ui/widgets/source/widgets/wcm/ContentFinder.js", function(){
var s = MyClientLib.Sidekick;

var SK_INTERVAL = setInterval(function(){
var sk = CQ.WCM.getSidekick();

if(sk && sk.findById(s.SK_TAB_PANEL)){
clearInterval(SK_INTERVAL);
s.addTagsPanel(sk);
}
}, 250);
}).fail(function(jqxhr, settings, exception){
console.log("Error parsing /libs/cq/ui/widgets/source/widgets/wcm/ContentFinder.js");
console.log(exception);
});
}

4) #133 we are loading ootb ContentFinder.js and #141 calling the js function to add a new tab and panel after #139 the sidekick is loaded and available

AEM CQ 56 - Modifying SiteAdmin Search Panel Grid Template Column

$
0
0

Goal


Here we are going to work on the Siteadmin Search Panel grid columns. By default in the search grid, template paths are shown



With changes explained in this post, we are going to modify the Template column to show template name in column and path in tooltip, as follows



Source code and package install are available for download

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

Solution


1) We need a servlet for template names returned as json

2) Search panel UI changes to modify the grid column

The Servlet


A simple Templates servlet is needed. With a JCR SQL query, it returns all templates in the system ordered by jcr:title. Add the following code to servlet source and deploy it as an OSGI component

package apps.mysample.siteadmin;

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.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.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.servlet.ServletException;
import java.io.IOException;

@SlingServlet(
paths="/bin/mycomponents/templates",
methods = "GET",
metatype = false,
label = "Templates Servlet"
)
public class Templates extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(Templates.class);

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

JSONWriter jw = new JSONWriter(response.getWriter());

try{
ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);

QueryManager qm = session.getWorkspace().getQueryManager();

jw.object();
jw.key("data").array();

String stmt = "select * from cq:Template order by jcr:title";

Query q = qm.createQuery(stmt, Query.SQL);

NodeIterator results = q.execute().getNodes();
Node node = null;

while(results.hasNext()){
node = results.nextNode();

jw.object();
jw.key("id").value(node.getPath());
jw.key("name").value(node.getProperty("jcr:title").getString());
jw.endObject();
}

jw.endArray();
jw.endObject();
}catch(Exception e){
log.error("Error getting templates",e);
throw new ServletException(e);
}
}
}


UI Changes


1) Siteadmin Search Panel is created using the logic in /libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js. Access CRXDE Lite (http://localhost:4502/crx/de/index.jsp), overly this file by creating /apps/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SearchPanel = {
templatesStore: null,
SA_SEARCH_PANEL_GRID: "cq-siteadminsearchpanel-grid",

setTemplateName: function(grid){
//read the templates when siteadmin is loaded in browser
if(!this.templatesStore){
this.templatesStore = new CQ.Ext.data.Store({
proxy: new CQ.Ext.data.HttpProxy({
"autoLoad":false,
url: "/bin/mycomponents/templates.json",
method: 'GET'
}),
reader: new CQ.Ext.data.JsonReader({
root: 'data',
fields: [
{name: 'id', mapping: 'id'},
{name: 'name', mapping: 'name'}
]
})
});

this.templatesStore.load();
}

//get the template column
var tColumn = grid.getColumnModel().getColumnById("template");
var store = this.templatesStore;

if(tColumn){
tColumn.renderer = function(v, params, record) {
var template = store.getById(v);

//set the path as tooltip
params.attr = ' ext:qtip="' + v + '"';

//return template name
return template ? template.data.name : v;
};
}

grid.doLayout();
}
};

$.getScript("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js", function(){
var s = MyClientLib.SearchPanel;

if(window.location.pathname == "/siteadmin"){
var SA_INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp(s.SA_SEARCH_PANEL_GRID);

if(grid){
clearInterval(SA_INTERVAL);
s.setTemplateName(grid);
}
}, 250);
}
}).fail(function(jqxhr, settings, exception){
console.log("Error parsing /libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js");
console.log(exception);
});

AEM CQ 56 - Multi Image Component

$
0
0

Goal


Create a multi image component for dynamically adding images. The component dialog provides icons for adding/removing/reordering images. Check demo video



This is a work in progress and not production ready ( install package not provided ) with few bugs.

1) Reorder is not yet implemented
2) Refresh page is needed sometimes, after adding a new image
3) Crop parameters are not saved sometimes

Prerequisites, References


If you are new to CQ

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

2) Read this postfor image component customization

Solution


1) Create folder /apps/multifieldimage and copy /libs/foundation/components/logo to /apps/multifieldimage; rename /apps/multifieldimage/logo to /apps/multifieldimage/image and logo.jsp to image.jsp

2) Replace the content of /apps/multifieldimage/image/dialog with following xml code

<?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"
jcr:primaryType="cq:Dialog"
activeTab="{Long}0"
title="Logo (Design)"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<basic
jcr:primaryType="cq:Widget"
title="Images"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<images
jcr:primaryType="cq:Widget"
border="false"
hideLabel="false"
imagePoolMax="8"
name="./images"
xtype="multiimagewidget">
<imageConfig
jcr:primaryType="cq:Widget"
cropParameter="imageCrop"
ddGroups="[media]"
fileNameParameter="imageName"
fileReferenceParameter="imageReference"
height="200"
mapParameter="imageMap"
name="image"
parentPrefix="demo"
rotateParameter="imageRotate"
sizeLimit="100"/>
</images>
</items>
</basic>
</items>
</jcr:root>


3) Line #17 imagePoolMax is a configurable parameter for the maximum number of images a user can dynamically add in a dialog

4) Create a clientlib /apps/multifieldimage/image/clientlib/multiimage.js and add the following code

var MyClientLib = MyClientLib || {};

CQ.Ext.override(CQ.html5.form.SmartImage, {
syncFormElements: function() {
if(!this.fileNameField.getEl().dom){
return;
}

CQ.html5.form.SmartImage.superclass.syncFormElements.call(this);

var toolCnt = this.imageToolDefs.length;

for (var toolIndex = 0; toolIndex < toolCnt; toolIndex++) {
var toolToProcess = this.imageToolDefs[toolIndex];
toolToProcess.transferToField();
}
} ,

processRecord: function(record, path){
CQ.html5.form.SmartImage.superclass.processRecord.call(this,record, path);
var imagePanel = this.ownerCt;

if(record.data[imagePanel.imageConfig.parentName]){
imagePanel.setVisible(true);

var widget = imagePanel.ownerCt;

if(widget.imagePanels[0] == imagePanel){
return;
}

var poolPanels = widget.poolPanels;

widget.poolPanels = poolPanels.splice(1, poolPanels.length );
widget.imagePanels.push(imagePanel);
}
}
});

MyClientLib.MultiImageWidget = CQ.Ext.extend(CQ.Ext.Panel, {
BASE_ID : 'MultiImageWidgetPanel',
imagePanels: [],
poolPanels: [],

constructor: function(config){
config = config || {};

var defaults = { "layout" : "form", border: false };
config = CQ.Util.applyDefaults(config, defaults);

MyClientLib.MultiImageWidget.superclass.constructor.call(this, config);
},

getImageConfig: function(suffix){
var config = CQ.Util.copyObject(this.imageConfig);

config.id = this.BASE_ID + "-" + suffix;

var parentPrefix = config.parentPrefix;

if(!parentPrefix){
parentPrefix = "demo";
}

parentPrefix = parentPrefix + "-" + suffix;
config.parentName = parentPrefix;

var changeParams = ["cropParameter", "fileNameParameter","fileReferenceParameter","mapParameter","rotateParameter","name"];

CQ.Ext.each(changeParams, function(cItem){
config[cItem] = "./" + parentPrefix + "/" + config[cItem];
});

config.xtype = "html5smartimage";

return config;
},

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

var imagePoolMax = this.imagePoolMax;

if(!imagePoolMax){
this.imagePoolMax = 10;
}

var suffix = 1;

var image = new MyClientLib.MultiImage({imageConfig : this.getImageConfig(suffix++)});
this.imagePanels.push(image);
this.add(image);

var pooledImage;

for(var i = 0; i < this.imagePoolMax - 1; i++){
pooledImage = new MyClientLib.MultiImage({imageConfig : this.getImageConfig(suffix++)});
pooledImage.setVisible(false);

this.poolPanels.push(pooledImage);
this.add(pooledImage);
}

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

dialog.on('beforesubmit', function(){
CQ.Ext.each(this.poolPanels, function(i){
widget.remove(i, true);
});
},this);
},

setValue: function (value) {
}
});

CQ.Ext.reg("multiimagewidget", MyClientLib.MultiImageWidget);

MyClientLib.MultiImage = CQ.Ext.extend(CQ.Ext.Panel, {
imageConfig: '',
style: "margin-bottom: 10px",

tools: [{
id: "plus",
handler: function(e, toolEl, panel, tc){
var widget = panel.ownerCt;
var poolPanels = widget.poolPanels;

var image = poolPanels[0];
image.setVisible(true);

widget.poolPanels = poolPanels.splice(1, poolPanels.length );
widget.imagePanels.push(image);
}
},{
id: "toggle",
handler: function(e, toolEl, panel, tc){
alert("Reorder up is a work in progress, and the icon is different as the .x-tool-top is not available in cq css")
}
},{
id: "down",
handler: function(e, toolEl, panel, tc){
alert("Reorder down is a work in progress")
}
},{
id: "minus",
handler: function(e, toolEl, panel, tc){
var widget = panel.ownerCt;

var image = panel.find('xtype', 'html5smartimage')[0];
var path = image.dataPath + "/" + panel.imageConfig.parentName;

$.ajax({
url: path,
dataType: "json",
type: 'DELETE',
success: function(data){

}
});

widget.remove(panel, true);
}
}],

constructor: function(config){
config = config || {};

MyClientLib.MultiImage.superclass.constructor.call(this, config);
},

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

var config = this.imageConfig;
this.add(config);
},

validate: function(){
return true;
},

getName: function(){
return this.name;
}
});

CQ.Ext.reg("multiimagepanel", MyClientLib.MultiImage);

5) Add the following code to /apps/multifieldimage/image/image.jsp

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

<%@ page import="java.util.Iterator" %>

<%
Iterator<Resource> children = resource.listChildren();
Resource res = null;

if(!children.hasNext()){
%>
Configure Images

<%
}

while(children.hasNext()){
res = children.next();
%>
Image

<img src="<%=res.adaptTo(Node.class).getProperty("imageReference").getString()%>">

<%
}
%>

6) The component structure



AEM CQ 56 - Adding Column to SiteAdmin Grid

$
0
0

Goal


Lets add a column to SiteAdmin (http://localhost:4502/siteadmin) grid. The Template Path column we are going to add displays template path of page

Default SiteAdmin Grid



Customized SiteAdmin Grid



Related Posts


If you are trying to modify a column of grid, check this post

Solution


1) Use overlay architecture of CQ to overlay the file /libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.js. Create file /apps/cq/ui/widgets/source/widgets/wcm/SiteAdmin.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SiteAdminGrid = {
SA_GRID: "cq-siteadmin-grid",

addPathColumn: function(grid){
var cm = grid.getColumnModel();

cm.columns.push({
"header": "Template Path",
"id":"templatePath",
"dataIndex":"templatePath",
"renderer": function(v, params, record) {
return v;
}
});

grid.doLayout();
}
};

CQ.shared.HTTP.get("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdmin.js");

(function(){
var s = MyClientLib.SiteAdminGrid;

if(window.location.pathname == "/siteadmin"){
var SA_INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp(s.SA_GRID);

if(grid && ( grid.rendered == true)){
var cm = grid.getColumnModel();

if(cm && cm.columns){
clearInterval(SA_INTERVAL);
s.addPathColumn(grid);
}
}
}, 250);
}
})();

2) Here we are loading ootb SiteAdmin.js to create the default siteadmin grid (line 22) and later add the path column

3) The overlay in CRXDE Lite (http://localhost:4502/crx/de)



Open Content Finder Images in New Tab

$
0
0

Goal


The CQ product Content Finder (http://localhost:4502/cf#/content/geometrixx/en.html) images results view has no action associated for image click. This post is about opening the image asset editor in new browser tab, when author double clicks on the image. Demo available for download

Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and overlay the file /libs/cq/ui/widgets/source/widgets/wcm/ContentFinder.js. To overlay, create file /apps/cq/ui/widgets/source/widgets/wcm/ContentFinder.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.ContentFinder = {
TAB_IMAGES: "cfTab-Images",

openImageInNewTab: function(){
var tab = CQ.Ext.getCmp(this.TAB_IMAGES);

if(!tab){
return;
}

var resultsView = tab.findByType("dataview");
resultsView = resultsView[0];

resultsView.on('dblclick', function(dView, index, node, eObj){
var sData = this.store.getAt(index);
window.open("/damadmin#" + sData.id);
});
}
};

CQ.shared.HTTP.get("/libs/cq/ui/widgets/source/widgets/wcm/ContentFinder.js");

(function(){
if( window.location.pathname == "/cf" ){
var INTERVAL = setInterval(function(){
var tabPanel = CQ.Ext.getCmp(CQ.wcm.ContentFinder.TABPANEL_ID);

if(tabPanel && (tabPanel.rendered == true)){
clearInterval(INTERVAL);
var c = MyClientLib.ContentFinder;
c.openImageInNewTab();
}
}, 250);
}
})();

2) Line 23, we are loading the ootb ContentFinder and using JS function openImageInNewTab(), associating double click action to images

3) The overlay in CRXDE Lite

Adding Modified By Filter to SiteAdmin Search Panel

$
0
0

Goal


This post is on extending the SiteAdmin Search Panel (http://localhost:4502/siteadmin) to provide additional filters for search and filtering content. Here we add Modified By filter ( predicate ) and by default, the filter is set to logged-in user. So when author clicks on search tab, all pages modified by the author are shown. See the demo



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de); Overlay the file /libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js by creating /apps/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SiteAdminSearchPanel = {
SA_SEARCH_PANEL_MB: 'cq-myclientlib-searchpanel-modified-by',
SA_SEARCH_PANEL_SEARCH: "cq-siteadminsearchpanel-search",
SA_SEARCH_PANEL_GRID: "cq-siteadminsearchpanel-grid",
SA_SEARCH_PANEL: "cq-siteadminsearchpanel",

addModifiedBy: function(){
var panel = CQ.Ext.getCmp(this.SA_SEARCH_PANEL_SEARCH);

var defaults = {
"predicateName":"property",
"propertyName":"jcr:content/cq:lastModifiedBy"
};

var id = CQ.wcm.PredicateBase.createId(defaults.predicateName);

panel.add(new CQ.Ext.form.Hidden({
"name": id,
"value": defaults.propertyName
}));

var aCombo = new CQ.security.AuthorizableSelection({
id: this.SA_SEARCH_PANEL_MB,
"name": id + ".value",
"anchor": "100%",
"valueField" : "id",
"displayField" : "name",
"fieldLabel": CQ.I18n.getMessage("Modified By"),
"filter" : "users",
"autoSelect" : true
});

panel.add(aCombo);
panel.doLayout();

var sPanel = CQ.Ext.getCmp(this.SA_SEARCH_PANEL);
sPanel.reloadPages();
}
};

CQ.shared.HTTP.get("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js");

(function(){
if(window.location.pathname == "/siteadmin"){
var s = MyClientLib.SiteAdminSearchPanel;

CQ.Ext.override(CQ.wcm.SiteAdminSearchPanel, {
reloadPages: function(){
this.performReset();

var modBy = CQ.Ext.getCmp(s.SA_SEARCH_PANEL_MB);

if(modBy){
modBy.setValue(CQ.shared.User.data["userID"]);
this.performSearch();
}
}
});

var INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp(s.SA_SEARCH_PANEL_GRID);

if(grid && (grid.rendered == true)){
clearInterval(INTERVAL);
s.addModifiedBy();
}
}, 250);
}
})();

2) Line 43, we are loading the ootb SiteAdminSearchPanel.js and Line 65, after search panel grid renders, add the Modified By filter

3) Line 50, reloadPages() is called when user clicks on search tab; logic in JS function adds the Modified By filter value to search params

4) The overlay in CRXDE Lite


AEM CQ 56 - Add Template as Options Predicate in SiteAdmin Search Panel

$
0
0

Goal


CQ SiteAdmin Search Panel (http://localhost:4502/siteadmin) lets author filter content using Template name. Default template filter (predicate) is a textbox, with this customization we hide ootb filter and add a new options filter showing available /apps/ templates in CQ system. Package install, Source code and Demo available for download

From the product



Customized



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

Solution


1) Create a servlet apps.mysample.searchpanel.GetTemplates for getting the templates in CQ system as json, readable by CQ.wcm.OptionsPredicate and install it as OSGI component. Add the following code in servlet

package apps.mysample.searchpanel;

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.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.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.servlet.ServletException;
import java.io.IOException;

@SlingServlet(
paths="/bin/mycomponents/searchpanel/templates",
methods = "GET",
metatype = false,
label = "Get Templates Servlet"
)
public class GetTemplates extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(GetTemplates.class);

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

JSONWriter jw = new JSONWriter(response.getWriter());

try{
ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();

String stmt = "select * from cq:Template where jcr:path like '/apps/%' order by jcr:title";
Query q = qm.createQuery(stmt, Query.SQL);

NodeIterator results = q.execute().getNodes();
Node node = null ; Property title = null;

jw.object();
jw.key("jcr:title");
jw.value("Templates");

while(results.hasNext()){
node = results.nextNode();

if(!node.hasProperty("jcr:title")){
continue;
}

title = node.getProperty("jcr:title");

jw.key(node.getPath());
jw.object();
jw.key("jcr:title");
jw.value(title.getString());
jw.key("tagId");
jw.value(node.getPath());
jw.endObject();
}

jw.endObject();
}catch(Exception e){
log.error("Error getting templates",e);
throw new ServletException(e);
}
}
}

2) To extend ootb SiteAdmin Search Panel overlay /libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js by creating /apps/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.SiteAdminSearchPanel = {
SA_SEARCH_PANEL_SEARCH: "cq-siteadminsearchpanel-search",
SA_SEARCH_PANEL_GRID: "cq-siteadminsearchpanel-grid",

addTemplateOptions: function(){
var panel = CQ.Ext.getCmp(this.SA_SEARCH_PANEL_SEARCH);

//search for template field in search panel
var component = panel.findBy(function(comp){
return comp["propertyName"] == "jcr:content/cq:template";
}, panel);

//hide ootb template field in search panel
component[0].setVisible(false);

//add the templates predicate
var templates = new CQ.wcm.OptionsPredicate({
collapse: "level0",
hideLabel: true,
"jcr:primaryType" : "cq:Widget",
optionsPaths: ["/bin/mycomponents/searchpanel/templates.json"],
"propertyName":"jcr:content/cq:template"
});

panel.add(templates);

var subPanel = templates.findBy(function(comp){
return comp["subPanel"] instanceof CQ.Ext.Panel ;
}, panel)[0];

//get the checkboxes
var cBoxes = subPanel.findBy(function(comp){
return comp instanceof CQ.Ext.form.Checkbox;
}, panel);

//add toottip showing template path
CQ.Ext.each(cBoxes, function(box){
box.on('afterrender', function(){
CQ.Ext.QuickTips.register({
target: this.id,
text: this.inputValue,
dismissDelay: 2000
});
});
});

panel.doLayout();
}
};

//Load ootb SiteAdminSearchPanel
CQ.shared.HTTP.get("/libs/cq/ui/widgets/source/widgets/wcm/SiteAdminSearchPanel.js");

(function(){
if(window.location.pathname == "/siteadmin"){
var SA_INTERVAL = setInterval(function(){
var s = MyClientLib.SiteAdminSearchPanel;
var grid = CQ.Ext.getCmp(s.SA_SEARCH_PANEL_GRID);

if(grid && (grid.rendered == true)){
clearInterval(SA_INTERVAL);
s.addTemplateOptions();
}
}, 250);
}
})();


3) The component install and overlay in CRXDE Lite (http://localhost:4502/crx/de)




AEM CQ 56 - Adding Images in MultiField

$
0
0

Goal


This post is on adding multiple images in a component dialog. Here we extend and use CQ.form.MultiField to add images of type CQ.html5.form.SmartImage. Source Code, Package Install and Demo Video are available for download





Prerequisities


If you are new to CQ visit this blog post; it explains page component basics and setting up your IDE

Create Component


1) In your CRXDE Lite http://localhost:4502/crx/de, create below folder and save changes

                      /apps/imagemultifield

2) Copy the component /libs/foundation/components/logo and paste it in path /apps/imagemultifield

3) Rename /apps/imagemultifield/logo to /apps/imagemultifield/imagemultifield

4) Rename /apps/imagemultifield/imagemultifield/logo.jsp to /apps/imagemultifield/imagemultifield/imagemultifield.jsp

5) Change the following properties of /apps/imagemultifield/imagemultifield

                     componentGroup - My Components
                     jcr:title - Image MultiField Component

6) Add the following to dialog (/apps/imagemultifield/imagemultifield/dialog) xml

<?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"
jcr:primaryType="cq:Dialog"
activeTab="{Long}0"
title="Multi Image"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<basic
jcr:primaryType="cq:Widget"
title="Images"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<images
jcr:primaryType="cq:Widget"
border="false"
hideLabel="true"
name="./images"
xtype="imagemultifield">
<fieldConfig
jcr:primaryType="cq:Widget"
border="false"
hideLabel="true"
layout="form"
padding="10px 0 0 100px"
xtype="imagemultifieldpanel">
<items jcr:primaryType="cq:WidgetCollection">
<image
jcr:primaryType="cq:Widget"
cropParameter="./imageCrop"
ddGroups="[media]"
fileNameParameter="./imageName"
fileReferenceParameter="./imageReference"
height="250"
mapParameter="./imageMap"
name="./image"
rotateParameter="./imageRotate"
sizeLimit="100"
xtype="imagemultifieldsmartimage"/>
</items>
</fieldConfig>
</images>
</items>
</basic>
</items>
</jcr:root>

8) Line 18, create an instance of widget type imagemultifield ( ImageMultiField.MultiField extending CQ.form.MultiField) explained in the next section

9) Line 25, 38, when a user clicks on Add Item of imagemultifield, create a panel of type imagemultifieldpanel (ImageMultiField.Panel extending CQ.Ext.Panel); in the panel created, add image widget imagemultifieldsmartimage ( ImageMultiField.SmartImage extending CQ.html5.form.SmartImage)

Add JS Logic and Register XTypes


1) Create node /apps/imagemultifield/imagemultifield/clientlib of type cq:ClientLibraryFolder and add the following properties

             categories - String - cq.widgets

2) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/js.txt and add the following

              imagemultifield.js

3) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/imagemultifield.js and add the following code

CQ.Ext.ns("ImageMultiField");

ImageMultiField.Panel = CQ.Ext.extend(CQ.Ext.Panel, {
initComponent: function () {
ImageMultiField.Panel.superclass.initComponent.call(this);

var multifield = this.findParentByType('imagemultifield');
var image = this.find('xtype', 'imagemultifieldsmartimage')[0];

var imageName = multifield.nextImageName;

if(!imageName){
imageName = image.name;

if(!imageName){
imageName = "demo";
}else if(imageName.indexOf("./") == 0){
imageName = imageName.substr(2); //get rid of ./
}

var suffix = multifield.nextImageNum = multifield.nextImageNum + 1;
imageName = this.name + "/" + imageName + "-" + suffix;
}

image.name = imageName;

var changeParams = ["cropParameter", "fileNameParameter","fileReferenceParameter",
"mapParameter","rotateParameter" ];

CQ.Ext.each(changeParams, function(cItem){
if(image[cItem]){
image[cItem] = imageName + "/" +
( image[cItem].indexOf("./") == 0 ? image[cItem].substr(2) : image[cItem]);
}
});

CQ.Ext.each(image.imageToolDefs, function(toolDef){
toolDef.transferFieldName = imageName + toolDef.transferFieldName.substr(1);
toolDef.transferField.name = toolDef.transferFieldName;
});
},

setValue: function (record) {
var multifield = this.findParentByType('imagemultifield');
var image = this.find('xtype', 'imagemultifieldsmartimage')[0];

var recCopy = CQ.Util.copyObject(record);

var imagePath = multifield.path + "/" + image.name;
var imgRec = recCopy.get(image.name);

for(var x in imgRec){
if(imgRec.hasOwnProperty(x)){
recCopy.data[x] = imgRec[x];
}
}

recCopy.data[this.name.substr(2)] = undefined;

var fileRefParam = image.fileReferenceParameter;
image.fileReferenceParameter = fileRefParam.substr(fileRefParam.lastIndexOf("/") + 1);

image.processRecord(recCopy, imagePath);
image.fileReferenceParameter = fileRefParam;
},

validate: function(){
return true;
}
});

CQ.Ext.reg("imagemultifieldpanel", ImageMultiField.Panel);

ImageMultiField.SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
syncFormElements: function() {
if(!this.fileNameField.getEl().dom){
return;
}

ImageMultiField.SmartImage.superclass.syncFormElements.call(this);
} ,

afterRender: function() {
ImageMultiField.SmartImage.superclass.afterRender.call(this);

var dialog = this.findParentByType('dialog');
var target = this.dropTargets[0];

if (dialog && dialog.el && target.highlight) {
var dialogZIndex = parseInt(dialog.el.getStyle("z-index"), 10);

if (!isNaN(dialogZIndex)) {
target.highlight.zIndex = dialogZIndex + 1;
}
}

var multifield = this.findParentByType('multifield');
multifield.dropTargets.push(target);

this.dropTargets = undefined;
}
});

CQ.Ext.reg('imagemultifieldsmartimage', ImageMultiField.SmartImage);

CQ.Ext.override(CQ.form.SmartImage.ImagePanel, {
addCanvasClass: function(clazz) {
var imageCanvas = CQ.Ext.get(this.imageCanvas);

if(imageCanvas){
imageCanvas.addClass(clazz);
}
},

removeCanvasClass: function(clazz) {
var imageCanvas = CQ.Ext.get(this.imageCanvas);

if(imageCanvas){
imageCanvas.removeClass(clazz);
}
}
});

CQ.Ext.override(CQ.form.SmartImage.Tool, {
processRecord: function(record) {
var iniValue = record.get(this.transferFieldName);

if(!iniValue && ( this.transferFieldName.indexOf("/") !== -1 )){
iniValue = record.get(this.transferFieldName.substr(this.transferFieldName.lastIndexOf("/") + 1));
}

if (iniValue == null) {
iniValue = "";
}

this.initialValue = iniValue;
}
});

CQ.Ext.override(CQ.form.MultiField.Item, {
reorder: function(item) {
if(item.field && item.field.xtype == "imagemultifieldpanel"){
var c = this.ownerCt;
var iIndex = c.items.indexOf(item);
var tIndex = c.items.indexOf(this);

if(iIndex < tIndex){ //user clicked up
c.insert(c.items.indexOf(item), this);
this.getEl().insertBefore(item.getEl());
}else{//user clicked down
c.insert(c.items.indexOf(this), item);
this.getEl().insertAfter(item.getEl());
}

c.doLayout();
}else{
var value = item.field.getValue();
item.field.setValue(this.field.getValue());
this.field.setValue(value);
}
}
});

ImageMultiField.MultiField = CQ.Ext.extend(CQ.form.MultiField , {
Record: CQ.data.SlingRecord.create([]),
nextImageNum: 0,
nextImageName: undefined,

initComponent: function() {
ImageMultiField.MultiField.superclass.initComponent.call(this);

var imagesOrder = new CQ.Ext.form.Hidden({
name: this.getName() + "/order"
});

this.add(imagesOrder);

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

dialog.on('beforesubmit', function(){
var imagesInOrder = this.find('xtype','imagemultifieldsmartimage');
var order = [];

CQ.Ext.each(imagesInOrder , function(image){
order.push(image.name.substr(image.name.lastIndexOf("/") + 1))
});

imagesOrder.setValue(JSON.stringify(order));
},this);

this.dropTargets = [];
},

addItem: function(value){
if(!value){
value = new this.Record({},{});
}
ImageMultiField.MultiField.superclass.addItem.call(this, value);
},

processRecord: function(record, path) {
if (this.fireEvent('beforeloadcontent', this, record, path) !== false) {
this.items.each(function(item) {
if(item.field && item.field.xtype == "imagemultifieldpanel"){
this.remove(item, true);
}
}, this);

var images = record.get(this.getName());
this.nextImageNum = 0;

if (images) {
var oName = this.getName() + "/order";
var oValue = record.get(oName) ? record.get(oName) : "";

var iNames = JSON.parse(oValue);
var highNum, val;

CQ.Ext.each(iNames, function(iName){
val = parseInt(iName.substr(iName.indexOf("-") + 1));

if(!highNum || highNum < val){
highNum = val;
}

this.nextImageName = this.getName() + "/" + iName;
this.addItem(record);
}, this);

this.nextImageNum = highNum;
}

this.nextImageName = undefined;

this.fireEvent('loadcontent', this, record, path);
}
}
});

CQ.Ext.reg('imagemultifield', ImageMultiField.MultiField);

4) In the above step we are adding necessary js logic to create a multifield of images; each item of multifield is a panel holding one smart image.So, when user clicks on Add Item a panel is created and added to multifield, smart image added to the panel. Call me if you have trouble understanding it, i have to do a lot of typing to explain the code :)

Content Structure in CRX


1) When you add images using the above imagemultifield with default configuration, the following content structure is created in CRX (/content/multi-image/jcr:content/par/imagemultifield)



2) The below node at level /content/multi-image/jcr:content/par/imagemultifield/images stores image order for rendering images in the order they were added or reordered



3)  Each image is added as a separate node under imagemultifield/images. For example here is node /content/multi-image/jcr:content/par/imagemultifield/images/image-2



Rendering Images


1) Images created in the CRX using MultiImage componet are rendered using /apps/imagemultifield/imagemultifield/imagemultifield.jsp. Add the following code in jsp

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

<%@ page import="java.util.Iterator" %>
<%@ page import="com.day.cq.wcm.foundation.Image" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>

<%
Iterator<Resource> children = resource.listChildren();

if(!children.hasNext()){
%>

Configure Images

<%
}else{
Resource imagesResource = children.next();
ValueMap map = imagesResource.adaptTo(ValueMap.class);
String order = map.get("order", String.class);

Image img = null; String src = null;
JSONArray array = new JSONArray(order);

for(int i = 0; i < array.length(); i++){
img = new Image(resource);
img.setItemName(Image.PN_REFERENCE, "imageReference");
img.setSuffix(String.valueOf(array.get(i)));
img.setSelector("img");

src = img.getSrc();
%>
<img src='<%=src%>'/>
<%
}
}
%>


2) Jsp renders, for example, the following image source paths

<imgsrc='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-1'/>
<imgsrc='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-2'/>
<imgsrc='/content/multi-image/_jcr_content/par/imagemultifield.img.png/image-3'/>

3) Add the following code in /apps/imagemultifield/imagemultifield/img.GET.java to get image binary

package apps.imagemultifield.imagemultifield;

import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.servlet.http.HttpServletResponse;

import com.day.cq.wcm.foundation.Image;
import com.day.cq.wcm.commons.AbstractImageServlet;
import com.day.image.Layer;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;

public class img_GET extends AbstractImageServlet {

protected Layer createLayer(ImageContext c)
throws RepositoryException, IOException {
return null;
}

protected void writeLayer(SlingHttpServletRequest req, SlingHttpServletResponse resp, ImageContext c, Layer layer)
throws IOException, RepositoryException {
Iterator<Resource> children = c.resource.listChildren();

if(!children.hasNext()){
return;
}

String rUri = req.getRequestURI();
String selImage = rUri.substring(rUri.lastIndexOf("/"));
Resource resource = req.getResourceResolver().getResource(children.next().getPath() + selImage);

if(resource == null){
return;
}

Image image = new Image(resource);
image.setItemName(Image.NN_FILE, "image");
image.setItemName(Image.PN_REFERENCE, "imageReference");

if (!image.hasContent()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

image.set(Image.PN_MIN_WIDTH, c.properties.get("minWidth", ""));
image.set(Image.PN_MIN_HEIGHT, c.properties.get("minHeight", ""));
image.set(Image.PN_MAX_WIDTH, c.properties.get("maxWidth", ""));
image.set(Image.PN_MAX_HEIGHT, c.properties.get("maxHeight", ""));

layer = image.getLayer(false, false, false);

boolean modified = image.crop(layer) != null;

modified |= image.resize(layer) != null;

modified |= image.rotate(layer) != null;

if (modified) {
resp.setContentType(c.requestImageType);
layer.write(c.requestImageType, 1.0, resp.getOutputStream());
} else {
Property data = image.getData();
InputStream in = data.getStream();
resp.setContentLength((int) data.getLength());
String contentType = image.getMimeType();

if (contentType.equals("application/octet-stream")) {
contentType=c.requestImageType;
}

resp.setContentType(contentType);
IOUtils.copy(in, resp.getOutputStream());

in.close();
}

resp.flushBuffer();
}
}

4) Finally, here is the component structure in CRX (http://localhost:4502/crx)





AEM CQ 56 - Adding RichTextEditor in MultiField with Drag&Drop Support

$
0
0

Goal


Create a Multifield (CQ.form.MultiField) with RichText Editor widgets (CQ.form.RichText). User should be able to drag and drop images from content finder onto the richtext editors of multifield. Source code, Package Install and Demo Video are available for download




Prerequisites


If you are new to CQ visit this blog post; it explains page component basics and setting up your IDE

Create Component


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/richtext-multifield

2) Copy /libs/foundation/components/text to /apps/richtext-multifield/text and change the componentGroup of text component to My Components

3) Add the following to dialog xml (/apps/richtext-multifield/text/dialog)

<?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:Dialog"
helpPath="en/cq/current/wcm/default_components.html#Text"
title="Text"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Widget"
anchor="100%"
title="Text"
xtype="panel">
<items jcr:primaryType="cq:WidgetCollection">
<text
jcr:primaryType="cq:Widget"
border="false"
hideLabel="{Boolean}true"
name="./text"
xtype="multifield">
<fieldConfig
jcr:primaryType="nt:unstructured"
xtype="mrichtext"/>
</text>
</items>
</tab1>
</items>
</jcr:root>

4) Create /apps/richtext-multifield/text/clientlib of type cq:ClientLibraryFolder and add property categories with value cq.widgets

5) Create /apps/richtext-multifield/text/clientlib/js.txt of type nt:file and add

                          richtextmultifield.js

6) Create /apps/richtext-multifield/text/clientlib/richtextmultifield.js of type nt:file and add the following JS logic.

CQ.Ext.ns("RichTextMultiField");

RichTextMultiField.RichText = CQ.Ext.extend(CQ.form.RichText, {
afterRender: function() {
RichTextMultiField.RichText.superclass.afterRender.call(this);

var dialog = this.findParentByType('dialog');
var target = this.dropTargets[0];

if (dialog && dialog.el && target.highlight) {
var dialogZIndex = parseInt(dialog.el.getStyle("z-index"), 10);

if (!isNaN(dialogZIndex)) {
target.highlight.zIndex = dialogZIndex + 1;
}
}

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

if(!multifield.dropTargets){
multifield.dropTargets = [];
}

multifield.dropTargets.push(target);
this.dropTargets = undefined;
},

syncValue: function() {
if(!this.el || !this.el.dom){
return;
}
RichTextMultiField.RichText.superclass.syncValue.call(this);
}
});

CQ.Ext.reg('mrichtext', RichTextMultiField.RichText);

7) Line 36, we are extending the ootb richtext with necessary logic and registering as new xtype mrichtext

8) Add the following logic in /apps/richtext-multifield/text/text.jsp

<%@ page import="org.apache.commons.lang3.ArrayUtils" %>
<%@include file="/libs/foundation/global.jsp" %>

<%
String[] texts = properties.get("text", String[].class);

if(ArrayUtils.isEmpty(texts)){
%>
Configure Texts
<%
}else{
for(String text : texts){
%>
<%= text %>
<%
}
}

%>

9) The component structure in CRXDE Lite






AEM CQ 56 - Extend UserAdmin, add new User Properties

$
0
0

Goal


Extend UserAdmin console and add new user properties. Here we add a new property Second Email, stored in CRX as secondEmail

From the Product



Customized console




In CRX



Solution - 1


1) Create a cq:ClientLibraryFolder /apps/myclientlib and add property categories cq.security of type String

2) Create /apps/myclientlib/js.txt of type nt:file and add the following

                  UserProperties.js

3) Create /apps/myclientlib/UserProperties.js of type nt:file and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.UserAdmin = {
addSecondEmail: function(propPanel){
var userForm = propPanel.userForm;
var emailComp = userForm.find('name', 'email');

var secondEmailComp = {
"xtype":"textfield",
"fieldLabel": "Second Email",
"anchor":"100%",
"name":"secondEmail"
};

userForm.insert(userForm.items.indexOf(emailComp[0]) + 1, secondEmailComp);
userForm.doLayout();
}
};

(function(){
if(window.location.pathname == "/useradmin"){
var fields = CQ.security.data.AuthRecord.FIELDS;
fields.push({"name": "secondEmail"});

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

if(userAdmin && userAdmin.userProperties){
clearInterval(UA_INTERVAL);
MyClientLib.UserAdmin.addSecondEmail(userAdmin.userProperties);
}
}, 250);
}
})();


Solution - 2


Not preferred; Login to CRXDE Lite (http://localhost:4502/crx/de), Overlay file /libs/cq/security/widgets/source/widgets/security/UserProperties.js by creating /apps/cq/security/widgets/source/widgets/security/UserProperties.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.UserAdmin = {
addSecondEmail: function(propPanel){
var userForm = propPanel.userForm;
var emailComp = userForm.find('name', 'email');

var secondEmailComp = {
"xtype":"textfield",
"fieldLabel": "Second Email",
"anchor":"100%",
"name":"secondEmail"
};

userForm.insert(userForm.items.indexOf(emailComp[0]) + 1, secondEmailComp);
userForm.doLayout();
}
};

CQ.shared.HTTP.get("/libs/cq/security/widgets/source/widgets/security/UserProperties.js");

(function(){
if(window.location.pathname == "/useradmin"){
var fields = CQ.security.data.AuthRecord.FIELDS;
fields.push({"name": "secondEmail"});

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

if(userAdmin && userAdmin.userProperties){
clearInterval(UA_INTERVAL);
MyClientLib.UserAdmin.addSecondEmail(userAdmin.userProperties);
}
}, 250);
}
})();

AEM CQ 56 - Disable Component (EditRollOver) Menu Delete

$
0
0

Goal


This post can be useful if you'd like to disable some of the menu options of components added to par on a page, for example, you might want to disable Delete option for targeted components on a page, to avoid deletion by authors (An other ideal way is to statically include the component) Check the demo




Solution


1) Create folder /apps/clientlibs

2) Create /apps/clientlibs/myclientlib of type cq:ClientLibraryFolder and add property categories with String value cq.widgets

3) Create /apps/clientlibs/myclientlib/js.txt of type nt:file, add the following

                       disableDelete.js

4) Create /apps/clientlibs/myclientlib/disableDelete.js of type nt:file and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.EditRollover = CQ.Ext.extend(CQ.wcm.EditRollover, {
handleContextMenu: function(e){
MyClientLib.EditRollover.superclass.handleContextMenu.call(this, e);

var component = this.element.linkedEditComponent;

if (!component || !component.menuComponent) {
return;
}

var menu = component.menuComponent;
var dTargeting = menu.find('text', "Disable targeting");

//if Disable targeting menu option doesn't exist, donot disable Delete
if(!dTargeting || dTargeting.length == 0){
return;
}

var del = menu.find('text', "Delete");

if(del && del.length > 0){
del[0].setDisabled(true);
}
}
});

CQ.Ext.reg("editrollover", MyClientLib.EditRollover);

AEM CQ 56 - Crop Images in a Workflow Step

$
0
0

Goal


Create workflow step to crop images. The cropped image is saved as payload image rendition. Check Demo VideoSource code and Package Install


Solution


For creating workflow, we use the Dialog Participant Step and configure dialog with a CQ.html5.form.SmartImage widget. User selects crop co-ordinates; the next automated step in workflow reads crop numbers, crops the image and saves it as logo.png renditioon

Create Wokflow Step


1) Create and deploy workflow step ImageCropStep as OSGI bundle, to read crop co-ordinates and crop the image ( Read this post on how to create an OSGI component )

package apps.mysample.imagecrop;

import com.day.cq.commons.ImageHelper;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
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 com.day.image.Layer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.*;
import java.awt.*;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

@Component(metatype = true)
@Service
@Property(name = "process.label", value = "Crop Image In Inbox")
public class ImageCropStep implements WorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(ImageCropStep.class);

@Reference(policy = ReferencePolicy.STATIC)
private JcrResourceResolverFactory factory;


@Override
public void execute(WorkItem item, WorkflowSession session, MetaDataMap metaData)
throws WorkflowException {
try{
createCroppedRendition(item, session);
}catch(Exception e){
log.error("error generating cropped rendition", e);
throw new WorkflowException("Crop failed", e);
}
}

private void createCroppedRendition(WorkItem item, WorkflowSession session) throws Exception {
Resource resource = getResourceFromPayload(item, session.getSession());
Resource dataResource = resource.getChild("jcr:content/renditions/original/jcr:content");

ValueMap map = resource.getChild("jcr:content").adaptTo(ValueMap.class);
String cords = map.get("imageCrop", String.class);

if(StringUtils.isEmpty(cords)){
log.warn("crop co-ordinates missing in jcr:content for: " + resource.getPath());
return;
}

Layer layer = ImageHelper.createLayer(dataResource);
Rectangle rect = ImageHelper.getCropRect(cords, resource.getPath());

layer.crop(rect);

OutputStream out = null;
InputStream in = null;
String mimeType = "image/png";

try {
File file = File.createTempFile("cropped", ".tmp");
out = FileUtils.openOutputStream(file);

layer.write(mimeType, 1.0, out);
IOUtils.closeQuietly(out);

in = FileUtils.openInputStream(file);
Asset asset = DamUtil.resolveToAsset(resource);

asset.addRendition("logo.png", in, mimeType);
session.getSession().save();
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(out);
}
}

private Resource getResourceFromPayload(WorkItem item, Session session) {
if (!item.getWorkflowData().getPayloadType().equals("JCR_PATH")) {
return null;
}

String path = item.getWorkflowData().getPayload().toString();
return factory.getResourceResolver(session).getResource(path);
}
}

2) Login to CRXDE Lite, create folders /apps/imagecropstep/apps/imagecropstep/install

3) Deploy bundle with ImageCropStep to /apps/imagecropstep/install

Create Dialog Image Widget


1) Create node /apps/imagecropstep/editor of type sling:Folder

2) Create node /apps/imagecropstep/editor/clientlib of type cq:ClientLibraryFolder and add property categories with String value cq.widgets

3) Create node /apps/imagecropstep/editor/clientlib/js.txt of type nt:file and add

                            SmartImage.js

4) Create node /apps/imagecropstep/editor/clientlib/SmartImage.js of type nt:file and add the following code

CQ.Ext.ns("MyClientLib");

CQ.Ext.override(CQ.form.SmartImage.ImagePanel, {
addCanvasClass: function(clazz) {
var imageCanvas = CQ.Ext.get(this.imageCanvas);

if(imageCanvas){
imageCanvas.addClass(clazz);
}
},

removeCanvasClass: function(clazz) {
var imageCanvas = CQ.Ext.get(this.imageCanvas);

if(imageCanvas){
imageCanvas.removeClass(clazz);
}
}
});

MyClientLib.InboxSmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
Record: CQ.data.SlingRecord.create([]),

constructor: function(config) {
config = config || {};
config.fileReferenceParameter = "imageReference";
config.name = "placeHolder";
MyClientLib.InboxSmartImage.superclass.constructor.call(this,config);
},

clearInvalid: function() {
if(!this.uploadPanel || !this.uploadPanel.body) {
return;
}

MyClientLib.InboxSmartImage.superclass.clearInvalid.call(this);
},

afterRender: function() {
MyClientLib.InboxSmartImage.superclass.afterRender.call(this);

var dialog = this.findParentByType('dialog');
dialog.setSize(900,550);

var imageAdded = false;

dialog.on('afterlayout', function(){
if(!imageAdded){
var rec = new this.Record({},{});

var inbox = CQ.Ext.getCmp(CQ.workflow.Inbox.List.ID);
var imagePath = inbox.getSelectionModel().getSelections()[0];
imagePath = imagePath.data.payloadPath;

rec.data["imageReference"] = imagePath;
this.processRecord(rec);

imageAdded = true;
}
},this);
}
});

CQ.Ext.reg('inboxsmartimage', MyClientLib.InboxSmartImage);


5) In the above step we are extending html5smartimage, registering it as inboxsmartimage, with necessary changes to load image available in the workflow payload ( Line 51-53 )

6) Create node /apps/imagecropstep/editor/dialog of type cq:Dialog and add the following xml chunk

<?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"
jcr:primaryType="cq:Dialog"
title="Image Dialog"
xtype="dialog">
<items jcr:primaryType="cq:WidgetCollection">
<image
jcr:primaryType="cq:Widget"
cropParameter="./jcr:content/imageCrop"
fieldLabel="Image"
height="250"
xtype="inboxsmartimage"/>
</items>
</jcr:root>


7) Here is the customization structure in CRX




Create Worflow


1) Access Workflow console (http://localhost:4502/libs/cq/workflow/content/console.html)

2) In Models tab click New and add title Inbox Crop

3) Double click Inbox Crop to open the workflow in a new tab ( http://localhost:4502/cf#/etc/workflow/models/inbox-crop.html ). Here is how it looks initially




4) Delete Step 1

5) From the SideKick -> Components tab -> Workflow, drag and drop the step Dialog Participant  Step



6) Double click to open the step and add

               Common tab -> Title: Select Crop Coordinates
               Common tab -> Description: In this step the user selects crop co-ordinates saved under jcr:content of payload image in dam
               User/Group tab -> User/Group: /home/groups/a/administrators
               Dialog tab -> Dialog path: /apps/imagecropstep/editor/dialog

7) Dialog path /apps/imagecropstep/editor/dialog is the dialog created in Step 6 of above section Create Dialog Image Widget



8) From the SideKick -> Components tab -> Workflow, drag and drop the step Process Step




9) Double click to open it and add

                             Common tab -> Title: Crop Image
                             Common tab -> Description: This automated step crops the image, reading crop coordinates from ./jcr:content/imageCrop and creates rendition /jcr:content/renditions/logo.png
                             Process tab -> Process: Select Crop Image In Inbox
                             Handler Advance: Check




10) The Process selected in Process tab above was deployed as OSGI bundle in section Create Wokflow Step above

11) Important, save the workflow




Start workflow on an Image


1) Access DAM admin console (http://localhost:4502/damadmin) and upload an image, say /Mine/Desert.jpg

2) Rightclick on Desert.jpg and click Workflow

3) Select Inbox Crop for Workflow, enter comment Complete this workflow step to crop image

4) Click Start

5) Access Inbox console (http://localhost:4502/inbox) and you should see the step Select Crop Coordinates




6) Rightclick on the step and select Complete

7) You should see the image ready for cropping, do the crop



8) Click Ok, step should complete, browse CRXDE Lite (http://localhost:4502/crx/de) /content/dam/Mine/Desert.jpg/jcr:content and you should see the crop co-ordinates with property imageCrop



9) The Crop Image in Inbox automated step should have created the cropped image as rendition /content/dam/Mine/Desert.jpg/jcr:content/renditions/logo.png



10) Access the cropped image to check if it is ok http://localhost:4502/content/dam/Mine/Desert.jpg/jcr:content/renditions/logo.png





AEM CQ 56 - Add New Column to SiteAdmin Search Panel

$
0
0

Goal


Add a new column to SiteAdmin Search Panel. Here the Status column we add, shows the scheduled activations/deactivations of a page. Package Install available for download



Related


1) Read this post on how to add a column to siteadmin grid

2) Check this post for modifying a column in siteadmin search panel grid


Solution



It's a two step process, first register servlet to get the page scheduled tasks as json, and second, add necessary UI changes


Deploy Servlet



1) Create and deploy servlet apps.mysample.searchpanelcolumn.GetScheduledActions as OSGI bundle; to get the scheduled activations/deactivations of page(s) as json. Add the following code..

package apps.mysample.searchpanelcolumn;

import com.day.cq.workflow.exec.Workflow;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.day.cq.workflow.status.WorkflowStatus;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.resource.ValueMap;
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.servlet.ServletException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;

@SlingServlet(
paths="/bin/mycomponents/schtasks",
methods = "GET",
metatype = false,
label = "Scheduled Activation Instances"
)
public class GetScheduledActions extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(GetScheduledActions.class);
private static SimpleDateFormat FORMATTER = new SimpleDateFormat("EEE, dd MMM, yyyy HH:mm");

private static final String ACTIVATE_MODEL = "/etc/workflow/models/scheduled_activation/jcr:content/model";
private static final String DEACTIVATE_MODEL = "/etc/workflow/models/scheduled_deactivation/jcr:content/model";

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

JSONWriter jw = new JSONWriter(response.getWriter());
String pageStr = request.getParameter("path");

try{
jw.object();

if(StringUtils.isEmpty(pageStr)){
jw.key("error").value("page input required");
jw.endObject();
return;
}

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

if(StringUtils.isEmpty(type)){
type = "ACTIVATE";
}

pageStr = pageStr.trim();
String[] pages = pageStr.split(",");

ResourceResolver resolver = request.getResourceResolver();
Resource resource = null;

WorkflowStatus wStatus = null;
List<Workflow> workflows = null;

Map<String, List<Map<String, String>>> retMap = new HashMap<String, List<Map<String, String>>>();
Map<String, String> map = null;
MetaDataMap mdMap = null;

String absTime = null, id = null, version = null;
List<Map<String, String>> list = null;

for(String page: pages){
if(StringUtils.isEmpty(page)){
continue;
}

resource = resolver.getResource(page);

if(resource == null){
continue;
}

wStatus = resource.adaptTo(WorkflowStatus.class);
workflows = wStatus.getWorkflows(false);

if(CollectionUtils.isEmpty(workflows)){
continue;
}

for (Workflow w: workflows) {
id = w.getWorkflowModel().getId();

if(type.equals("ACTIVATE") && !id.equals(ACTIVATE_MODEL)){
continue;
}else if(type.equals("DEACTIVATE") && !id.equals(DEACTIVATE_MODEL)){
continue;
}

list = retMap.get(page);

if(list == null){
list = new ArrayList<Map<String, String>>();
retMap.put(page, list);
}

map = new HashMap<String, String>();
list.add(map);

mdMap = w.getMetaDataMap();
absTime = mdMap.get("absoluteTime", String.class);

map.put("id", w.getId());
map.put("name", resource.getChild("jcr:content").adaptTo(ValueMap.class).get("jcr:title", String.class));
map.put("st",FORMATTER.format(w.getTimeStarted().getTime()));
map.put("ini", w.getInitiator());
map.put("type", id.equals(ACTIVATE_MODEL) ? "ACTIVATE" : "DEACTIVATE");
map.put("dt", FORMATTER.format(Long.parseLong(absTime)));
}
}

String path = null;
Iterator<Map<String, String>> itr = null;

jw.key("data").array();

for(Map.Entry<String, List<Map<String, String>>> entry : retMap.entrySet()){
list = entry.getValue();
path = entry.getKey();

itr = list.iterator();

while(itr.hasNext()){
jw.object();
jw.key("path").value(path);

for(Map.Entry<String, String> mEntry : itr.next().entrySet()){
jw.key(mEntry.getKey()).value(mEntry.getValue());
}

jw.endObject();
}
}

jw.endArray();
jw.endObject();
}catch(Exception e){
log.error("Error getting schedule activation instances",e);
throw new ServletException(e);
}
}
}

2) Login to CRXDE Lite, create folders /apps/searchpanelstatus/apps/searchpanelstatus/install

3) Deploy bundle with apps.mysample.searchpanelcolumn.GetScheduledActions to /apps/searchpanelstatus/install


UI Changes



1) Create folder (nt:folder) /apps/searchpanelstatus/clientlibs

2) Create folder /apps/searchpanelstatus/clientlibs/myclientlib of type cq:ClientLibraryFolder with property categories of type String set to cq.widgets

3) Add file of type nt:file /apps/searchpanelstatus/clientlibs/myclientlib/js.txt with statement

                                   addStatusColumn.js

4) Add file of type nt:file /apps/searchpanelstatus/clientlibs/myclientlib/addStatusColumn.js with following code...

CQ.Ext.ns("MyClientLib");

MyClientLib.SearchPanel = {
addStatusColumn: function(){
CQ.wcm.SiteAdminSearchPanel.COLUMNS["status"] = {
"header":CQ.I18n.getMessage("Status"),
"id":"status",
"dataIndex":"xxxxx", //some undefined, to workaround grid weird layout issue
"renderer": function(val, meta, rec) {
if(!rec.data.scheduledTasks){
return "";
}

var qtip = "<table class='qtip-table'><tr><th>" + CQ.I18n.getMessage("Task")
+ "</th><th>" + CQ.I18n.getMessage("Scheduled") + "</th><th>";

CQ.Ext.each(rec.data.scheduledTasks, function(t){
var iconCls = (t.type == 'ACTIVATE') ? "status status-scheduledtask-activation" :
"status status-scheduledtask-deactivation";

qtip = qtip + "<tr><td class='" + iconCls + "'></td><td>"
+ t.dt + " (" + t.ini + ")</td><td>";
});

qtip = qtip + "</table>";

return "<span class=\"status status-scheduledtask\" ext:qtip=\"" + qtip + "\"> </span>";
}
};

MyClientLib.SiteAdminSearchPanel = CQ.Ext.extend(CQ.wcm.SiteAdminSearchPanel, {
constructor: function(config) {
if (config.columns) {
config.columns.push({
"xtype" : "gridcolumn",
"usePredefined": "status"
});
}
MyClientLib.SiteAdminSearchPanel.superclass.constructor.call(this, config);
}
});

CQ.Ext.reg("siteadminsearchpanel", MyClientLib.SiteAdminSearchPanel);
},

addSchdTasksInStore: function(grid){
var store = grid.getStore();

store.on('load', function(s, recs){
var pages = "";
var updateRecs = {};

CQ.Ext.each(recs, function(r){
pages = pages + r.id + ",";
updateRecs[r.id] = r;
});

if(!pages){
return;
}

pages = pages.substr(0, pages.lastIndexOf(","));

$.ajax({
url: '/bin/mycomponents/schtasks',
dataType: "json",
type: 'GET',
async: false,
data: { "path" : pages, "type" : "ALL" },
success: function(data){
if(!data || !data["data"]){
return;
}

data = data["data"];
var rec;

CQ.Ext.each(data, function(d){
rec = updateRecs[d["path"]];

if(!rec.data.scheduledTasks){
rec.data.scheduledTasks = [];
}

rec.data.scheduledTasks.push(d);
});
}
});

grid.getView().refresh();
});
}
};

(function(){
MyClientLib.SearchPanel.addStatusColumn();

if(window.location.pathname == "/siteadmin"){
var SA_INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp("cq-siteadminsearchpanel-grid");

if(grid && (grid.rendered == true)){
clearInterval(SA_INTERVAL);
MyClientLib.SearchPanel.addSchdTasksInStore(grid);
}
}, 250);

}
})();


5) Here is the component structure in CRXDE Lite


AEM CQ 56 - Using HTML 5 Smart Image, Image Map Tools for Text Hot Spots

$
0
0

Goal


Create Text Hotspots on Images configured using HTML 5 Smart Image, Image Map tools. Check the demo, Demo on GeometrixxPackage Install of component available for download

This extension uses jquery position() function. When you have too many components/pars, in Edit mode, the hotspots may not appear in expected place, so use wcmmode=disabled in the browser or Sidekick Preview mode to verify if the hotspots are showing up in the right place. Make sure you do thorough testing on various screens/browsers/surfaces before you activate any pages with this component

Create Hotspots

                                                  


Display Hotspots with Text

                                                     


Solution


1) In CRXDE Lite, copy /libs/foundation/components/image to /apps/imagehotspot

2) Set the componentGroup property of /apps/imagehotspot/image to My Components

3) Set the jcr:title of /apps/imagehotspot/image to Image Hotspot

4) Add the following css to /apps/imagehotspot/image/clientlibs/image.css used to style the hovering texts on image

.spotText{
position: absolute;
color: white;
background: #222;
padding: 5px;
font-family: verdana;
font-size: 10px;
font-weight: bold;
opacity:0.6;
}

5) Create node /apps/imagehotspot/image/clientlibs/js.txt of type nt:file and add the following

                          textspots.js

6) Create node /apps/imagehotspot/image/clientlibs/textspots.js of type nt:file and add the following logic

var TextSpots = {
Spot: function(coords, title){
coords = coords.split(",");

//only circles are supported
if(!coords || coords.length !== 3){
return;
}

this.left = parseInt(coords[0]) + parseInt(coords[2]);
this.top = parseInt(coords[1]) + parseInt(coords[2]);
this.title = title;
},

getCircles: function(html){
var obj = $.parseHTML(html);
var spots = [];

if(!obj || (obj.length == 0)){
return;
}

$.each(obj[0].childNodes, $.proxy(function(i, v){
spots.push(new this.Spot(v.coords, v.title));
}, this));

return spots;
},

addHotSpots: function(id, circles){
var imageDiv = $("#" + id);
$.each(circles, function(i, c){
imageDiv.append($("<div>" + c.title + "</div>").addClass("spotText").css("top", c.top + "px").css("left", c.left + "px"));
});
}
};

7) Add the following code to /apps/imagehotspot/image/image.jsp

<%@include file="/libs/foundation/global.jsp" %>
<%@ page import="com.day.cq.wcm.foundation.Image,
java.io.PrintWriter" %>
<%@ page import="com.day.cq.wcm.foundation.ImageMap" %>

<%
try {
Resource res = null;

if (currentNode.hasProperty("fileReference")) {
res = resource;
}

if (res == null) {
%>
Configure Image
<%
} else {
Image img = new Image(res);
img.setItemName(Image.PN_REFERENCE, "fileReference");
img.setSelector("img");

String mapDefinition = properties.get(Image.PN_IMAGE_MAP, "");
ImageMap imageMap = ImageMap.fromString(mapDefinition);

String map = imageMap.draw("someid");
String src = img.getSrc();

%>
<div id="textOnImage">
<img src='<%=src%>'/>
</div>

<script>
$(function(){
var circles = TextSpots.getCircles('<%=map%>');
TextSpots.addHotSpots("textOnImage", circles);
});
</script>

<%
}
} catch (Exception e) {
e.printStackTrace(new PrintWriter(out));
}
%>

8) Finally, here is the component structure in CRXDE Lite

                                              



AEM CQ 56 - Open Sidekick Page Properties Dialog Programmatically

$
0
0

Goal


Open the Page properties dialog programmatically and not from Sidekick. Check the demo

Solution


In your page, add a button click listener with below logic

<input type=button onClick="openPagePropertiesDialog()" value="Open Page Properties"/>

<script>
function openPagePropertiesDialog(){
var sk = CQ.WCM.getSidekick();

if(!sk){
alert("Sidekick not available, is the sidekick fully loaded on page?");
return;
}

var pageTab = sk.findById("cq-sk-tab-PAGE");

var openDialog = function(){
var pagePropsButton = pageTab.findBy(function(comp){
return comp["text"] == "Page Properties...";
}, pageTab);

pagePropsButton[0].handler.call(sk);
};

//if sidekick is not expanded, expand it and later open the dialog
if(!pageTab){
var toggle = sk.tools["toggle"];
toggle.dom.click();

var SK_INTERVAL = setInterval(function(){
pageTab = sk.findById("cq-sk-tab-PAGE");

if(pageTab){
clearInterval(SK_INTERVAL);
openDialog();
}
}, 250);
}else{
openDialog();
}
}
</script>



AEM CQ 56 - Change Tabs Order In Content Finder

$
0
0

Goal


Change the order of tabs in Content Finder. Here we reverse the tabs order, not particularly useful, but it shows how you can change the order in case you need to

From the Product



Customized to Reverse



References


1) To extend page tab and add additional filters check this post

2)  For hiding unused tabs in content finder check this post

3) For showing additional image metadata in content finder, check this post

Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/reversecftabs

2) Create node /apps/reversecftabs/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/reversecftabs/clientlib/js.txt and add

                       reversetabs.js

4) Create file (nt:file) /apps/reversecftabs/clientlib/reversetabs.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.ContentFinder = CQ.Ext.extend(CQ.wcm.ContentFinder, {
getVisibleTabs: function(path) {
var tabs = MyClientLib.ContentFinder.superclass.getVisibleTabs.call(this, path);

//order is based on ranking, so change them
$.each(tabs, function(index, tab){
tab.ranking = tabs.length - index;
});

return tabs;
}
});

CQ.Ext.reg("contentfinder", MyClientLib.ContentFinder);


5) The component structure in CRXDE Lite

AEM CQ 56 - Useful Tips For Debugging

$
0
0
          
A very useful document, but lists commands on UNIX, if you are on windows

1) To check the number of sessions held in memory ( read this article for more detailed analysis on dealing with unclosed sessions ), in windows, get CQ process id


Run the jmap command of jvm

                               jmap -histo:live <<pid>> | findstr CRXSessionImpl

So in my CQ, as of this writing, there are 105 sessions (second column) still open in memory


2) To generate and check thread dumps, either use Threads tab of CQ5 Web Console http://localhost:4502/system/console or Jstack

Run the jstack command of jvm

                       jstack <<pid>> | findstr cq


AEM CQ 56 - Extend RichText Editor, add new Plugin PullQuote

$
0
0

Goal


Add a new plugin to RichText Editor. Here we add the Pull Quote plugin for inserting text that can be formatted/styled during component rendering. Source code, Package Install and Demo Video are available for download. Please leave a comment if you find bugs.




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/rte-pull-quote

2) Create clientlib (type cq:ClientLibraryFolder/apps/rte-pull-quote/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/rte-pull-quote/clientlib/js.txt, add the following

                         js/rtepullquote.js.js

4) Create folder (nt:folder) /apps/rte-pull-quote/clientlib/js

5) Create file (type nt:file) /apps/rte-pull-quote/clientlib/js/rtepullquote.js, add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.PullQuote = {
ADD_QUOTE_CMD : "addquote",
REMOVE_QUOTE_CMD : "removequote",
DEFAULT_PATTERN: "[pullquote:(align=<align>,text=<text>)]"
};

MyClientLib.PullQuote.Plugin = new Class({
toString: "PullQuotePlugin",
extend: CUI.rte.plugins.Plugin,
P: MyClientLib.PullQuote,

addQuoteUI: null,
removeQuoteUI: null,

getFeatures: function() {
return [ this.P.ADD_QUOTE_CMD, this.P.REMOVE_QUOTE_CMD ];
},

initializeUI: function(tbGenerator) {
var plg = CUI.rte.plugins;

if (this.isFeatureEnabled(this.P.ADD_QUOTE_CMD)) {
this.addQuoteUI = tbGenerator.createElement(this.P.ADD_QUOTE_CMD, this, true, "Add/Modify Pull Quote");
tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT,this.addQuoteUI,1000);
}

if (this.isFeatureEnabled(this.P.REMOVE_QUOTE_CMD)) {
this.removeQuoteUI = tbGenerator.createElement(this.P.REMOVE_QUOTE_CMD, this, true,"Remove Pull Quote");
tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT,this.removeQuoteUI,1001);
}
},

execute: function(cmd, value, env) {
if (cmd == this.P.ADD_QUOTE_CMD) {
this.showDialog(env.editContext);
}else {
this.editorKernel.relayCmd(MyClientLib.PullQuote.REMOVE_QUOTE_CMD, value);
}
},

showDialog: function(context) {
var editorKernel = this.editorKernel, dm = editorKernel.getDialogManager(), pattern = this.config.pattern;

if(!pattern){
pattern = this.P.DEFAULT_PATTERN;
}

var selection = CUI.rte.Selection.createProcessingSelection(context);
var rteText = selection.startNode.data, initValue = { align : "LEFT", text : "" };

if(rteText){
//this parsing logic depends on pattern, so when you add new pattern for pullquote make
//sure you modify the following code to suit your pattern
try{
var start = rteText.lastIndexOf("[pullquote:", selection.startOffset);

if( start !== -1 ){
var dIndex = rteText.indexOf(",");
initValue.align = rteText.substring(rteText.indexOf("=",start) + 1, dIndex);
initValue.text = rteText.substring(rteText.indexOf("=", dIndex) + 1, rteText.indexOf(")]"));
}
}catch(err){
CQ.Ext.Msg.alert("Error","Error parsing text with pattern : " + pattern);
}
}

var dialogConfig = {
"jcr:primaryType": "cq:Dialog",
title: "Pull Quote",
modal: true,
width: 600,
height: 400,
items: [ {
xtype: "panel",
layout: "fit",
padding: "20px 0 0 10px",
items: [ {
xtype: "panel",
layout: "form",
border: false,
items: [ {
xtype: 'radiogroup',
columns: 4,
fieldLabel: "Align",
items: [{
boxLabel: ' Left',
name: 'align',
value: 'LEFT',
checked: (initValue.align === "LEFT")
}, {
name: 'align',
boxLabel: ' Right',
value: 'RIGHT',
checked: (initValue.align === "RIGHT")
}, {
name: 'align',
boxLabel: ' Center',
value: 'CENTER',
checked: (initValue.align === "CENTER")
}, {
name: 'align',
boxLabel: ' Justify',
value: 'JUSTIFY',
checked: (initValue.align === "JUSTIFY")
}]
},{
xtype: "textarea",
height: 250,
name: "text",
fieldLabel: "Text",
fieldDescription: "Enter quote text",
anchor: "90%",
value: initValue.text
} ]
} ]
} ],
ok: function() {
var tBox = this.findByType("textarea")[0];
var rBox = this.findByType("radiogroup")[0];

if(!tBox.getValue()){
CQ.Ext.Msg.alert("Error","Enter text for quote");
return;
}

var value = {
text: tBox.getValue(),
align: rBox.getValue().value,
pattern: pattern
};

this.close();
editorKernel.relayCmd(MyClientLib.PullQuote.ADD_QUOTE_CMD, value);
},
listeners: {
show: function() {
editorKernel.fireUIEvent("dialogshow");
},
hide: function() {
editorKernel.fireUIEvent("dialoghide");
}
}
};

dm.show(CQ.WCM.getDialog(dialogConfig));
},

notifyPluginConfig: function(pluginConfig) {
pluginConfig = pluginConfig || { };

CUI.rte.Utils.applyDefaults(pluginConfig, {
"tooltips": {
"addquote": {
"title": "Add/Modify Pull Quote",
"text": "Add/Modify Pull Quote"
},
"removequote": {
"title": "Remove Pull Quote",
"text": "Remove Pull Quote"
}
}
});

this.config = pluginConfig;
},

updateState: function(selDef) {
var rteText = selDef.selection.startNode.data;

//this parsing logic depends on pattern, so when you add new pattern for pullquote make
//sure you modify the following code to suit your pattern
if(rteText && (rteText.lastIndexOf("[pullquote:", selDef.startOffset) !== -1)){
this.removeQuoteUI.setDisabled(false);
}else{
this.removeQuoteUI.setDisabled(true);
}

this.addQuoteUI.setSelected(false);
this.removeQuoteUI.setSelected(false);
}
});

CUI.rte.plugins.PluginRegistry.register("pullquote", MyClientLib.PullQuote.Plugin);

MyClientLib.PullQuote.Cmd = new Class({
toString: "PullQuote",
extend: CUI.rte.commands.Command,

P: MyClientLib.PullQuote,

isCommand: function(cmdStr) {
return (cmdStr == this.P.ADD_QUOTE_CMD) || (cmdStr == this.P.REMOVE_QUOTE_CMD);
},

getProcessingOptions: function() {
var cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_NODELIST;
},

addPullQuote: function(execDef){
var value = execDef.value, selection = execDef.selection;
var node = CUI.rte.DomProcessor.createNode(execDef.editContext, "span");

var rteText = selection.startNode.data;
var start = rteText ? rteText.lastIndexOf("[pullquote:", selection.startOffset) : -1;

if( start !== -1 ){
CUI.rte.Common.insertNode(node, selection.startNode, start);
selection.startNode.parentNode.removeChild(selection.startNode);
}else{
CUI.rte.Common.insertNode(node, selection.startNode, selection.startOffset);
}

if(value.pattern){
node.innerHTML = value.pattern.replace("<align>", value.align).replace("<text>", value.text);
}else{
node.innerHTML = "[pullquote:(align=\"" + value.align + "\",text=\"" + value.text + "\")]";
}
},

removePullQuote: function(execDef){
var selection = execDef.selection;

var rteText = selection.startNode.data;
var start = rteText.lastIndexOf("[pullquote:", selection.startOffset);

if( start !== -1 ){
selection.startNode.parentNode.removeChild(selection.startNode);
}
},

execute: function(execDef) {
if(execDef.command == this.P.ADD_QUOTE_CMD){
this.addPullQuote(execDef);
}else{
this.removePullQuote(execDef);
}
}
});

CUI.rte.commands.CommandRegistry.register("pullquote", MyClientLib.PullQuote.Cmd);

6) #185, #243 we are registering the plugin and necessary action command. #25, #30 creates the toolbar buttons for add, remove quotes

7) Let us add the not-so-great icons and necessary css. Create folder (nt:folder) /apps/rte-pull-quote/clientlib/themes

8) Create clientlib (type cq:ClientLibraryFolder/apps/rte-pull-quote/clientlib/themes/default and set a property categories of String type to cq.widgets

9) Create file (nt:file) /apps/rte-pull-quote/clientlib/themes/default/css.txt, add the following

                    css/rtepullquote.css

10) Create folder (nt:folder) /apps/rte-pull-quote/clientlib/themes/default/css and make sure you add the icons addquote.png and removequote.png

11) Create file (nt:file) /apps/rte-pull-quote/clientlib/themes/default/css/rtepullquote.css, add the following code. RTE looks for css classes x-edit-addquote and x-edit-removequote (for the plugin toolbar buttons added namely addquote and removequote )

#CQ .x-html-editor-tb .x-edit-addquote {
background: url(addquote.png) center no-repeat;
}

#CQ .x-html-editor-tb .x-edit-removequote {
background: url(removequote.png) center no-repeat;
}

12) Add any text component with RichText editor and in the rtePlugins path of dialog (eg. /apps/rte-pull-quote/text/dialog/items/tab1/items/text/rtePlugins) add the pullquote node to enable pull quote plugins



13) The component structure in CRXDE Lite




Viewing all 525 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>