Installing AEM from command line without sample content (Geometrixx)

Posted on Wednesday, November 27, 2013 By

When you install a new instance of Adobe Experience Manager a bunch of content is provided as Sample content. By sample content I am referring to the Geometrixx sites (Geometrixx, Geometrixx Mobile, Geometrixx Outdoors and Geometrixx Outdoors Mobile) that are provided as samples and references. They can be removed by uninstalling packages available in AEM. It is also a recommendation to Uninstall example content and users as part of the Adobe Experience Manager 5.6.1 Security Checklist specially in your Production environment.

The good part is that now we also have a  run mode option for installing or not installing the sample content while installing CQ from command line. These modes allow you to control the use of sample content. The sample content is defined before the quickstart is built and can include packages, configurations, etc:

Installation run modes are provided out-of-the-box:

  • author
  • publish

AND

  • samplecontent
  • nosamplecontent

 

Environment in which I have successfully used it had:

  • Oracle Linux
  • java version “1.6.0_65”
  • 64 bit VM

 

These are two pairs of mutually exclusive run modes; for example, you can:

  • define either author or publish, not both at the same time
  • combine author with either samplecontent or nosamplecontent (but not both)

 

Command line installation:

Note:

  • You will need to configure your -XX:MaxPermSize and Xmx based on 32bit or 64bit VM and how much memory you can use.
  • I have renamed my CQ 561 jar to cq56-author-p4502.jar for author instance and cq56-publish-p4503.jar for publish instance.

Author instance installation command:

java -server -XX:MaxPermSize=256m -Xmx1024M -jar cq56-author-p4502.jar -r author, nosamplecontent

Publish instance installation command:

java -server -XX:MaxPermSize=256m -Xmx1024M -jar cq56-publish-p4503.jar -r publish, nosamplecontent

Once CQ gets installed you verify that none of the Geometrixx related content should have got installed.

5:21 PM Permalink

How to fix AEM-SiteCatalyst connection problem

Posted on Monday, November 25, 2013 By

Cloud Serivces in CQ 5.5 / AEM 5.6 allows easy integration with other Adobe Marketing Cloud products. These enablements usually involve inputting credentials for an account that has API access privilege. Adobe SiteCatalyst is among one of these Cloud Services.

CQ5_Cloud_Services___Cloud_Services_Configurations

 

Lately I’ve seen customers not able to enable the integration even they have ensured that they put in the right credentials. They got frustrated and had to open support tickets with Adobe to address the problem. It turns out that majority of these connection issues can be fixed if you understand which SiteCatalyst data center you should be using, and how to change the configuration from within CQ/AEM.

Here I will walk you through the steps one by one:

  1. See if you can connect to SiteCatalyst using your provided credential (with Web API Access):
    Analytics20131125
  2. If you’re not able to log in, then try using the same credential to log into http://my.omniture.com or http://sc.omniture.com.
  3. Once you’re logged in, you should be landed on:
    • https://sc.omniture.com/sc15/reports/index.html?a=Overview.Site&<ignore_the_rest>
    • https://sc2.omniture.com/sc15/reports/index.html?a=Overview.Site&<ignore_the_rest>
    • https://sc3.omniture.com/sc15/reports/index.html?a=Overview.Site&<ignore_the_rest>
    • etc.
  4. See the pattern above? the subdomain (sc, sc2, sc3, sc4, www, www2, www3, www4, etc.) tells you exactly which Site Catalyst data center you’re connected to. You should use the same data center in CQ/AEM for the integration. However, CQ/AEM is defaulted to the San Jose data center (sc, or www) out of the box. If you’re using another data center, you need to configure CQ to point to the appropriate one.
  5. To do that, go to System Console in CQ/AEM at http://<domain>:<port>/system/console.
  6. Log in and go to the configuration page.
  7. And locate the Configuration “Day CQ Analytics SiteCatalyst HTTP Client”.
    AnalyticsConfig20131125
  8. The Data Center URLs is defaulted to https://api.omniture.com/admin/1.3/rest. Change it to the data center you should be connecting to. For example: https://api2.omniture.com/admin/1.3/rest/.
  9. Click Save.
  10. Now, go back to the Cloud Services Configuration and connect again with the same credential (CQ pointing to the correct SiteCatalyst data center now), you should be able to connect successfully.
    AnalyticsSuccess20131125
4:41 PM Permalink

AEM HealthCheck exercise

Posted on Sunday, November 24, 2013 By

Recently I stepped into a few engagements to assess and evaluate how CQ/AEM was implemented for customers, be the implementation done by in-house developers, or via a solution partner. Since the “health check” area is a little vague, I want to use this post to list some of the exercises usually covered by such engagements. Note that some of the following might not necessarily be covered in all engagements, it varies depending based on customers needs and time given.

  • Study the solution via written documentation or direct interviews with customers and system integrators.
  • Review user interface, identify opportunities to optimize the performance for various experiences (web, mobile, kiosk, etc.)
  • Review CQ authoring process and work with customer to streamline and optimize the authoring experience.
  • Provide general performance optimization strategies including but not limited to: caching, use of CDNs, delivering the right fidelity assets at the right time, etc.
  • Provide general architectural guidance (Gap analysis between reference architecture and customer’s implementation) and environment audit.
  • Provide development and deployment best practices, including but not limited to:
    • Review templates and components for reusability.
    • Review information architecture.
    • Review OSGi configurations.
    • Review build and package management tools, source control being used, etc.
  • Code review and scoring using code analysis tools.
  • Validate the application of security checklist and go-live checklist.

At the end of the exercise, Adobe Consulting Services will compile and consolidate all the information collected, and will provide a document on all the findings from the engagement as well as suggestions and next steps.

Healthcheck is usually carried out onsite, and it typically lasts from a few days to a few weeks.

3:03 PM Permalink

Custom Component: Manual List Widget

Posted on Friday, October 4, 2013 By

Requirement: The users want the ability to manually choose a list of of items from a source list of items.

Example: A list of all the articles in the system is the source list and the users want to select the most important ones to be listed on the home page of a site.

To achieve this, I created a custom ExtJs widget.

  • The users are able to double click on an item on the left (source) list to move the said item to the right list
  • They can manually order the items on the right list via drag and drop.
  • They can double click the item on the right list to remove it from the list
  • They are also able to pass tags to limit the results (your custom servlet that returns the JSON object to be consumed by the widget will need to handle this use case)

Below are the steps to use this widget in your implementation.

1. Paste the widget code from below into a JS file in your CQ implementation.

//Create a new class based on existing CompositeField
CQ.form.GridtoGrid = CQ.Ext.extend(CQ.CustomContentPanel, {
    secondGrid: null,
    firstGrid: null,
    columnModel: null,
    reader: null,
    layout: 'fit',
    constructor : function(config){

        var dataSrcUrl;

        if( config.dataSrcUrl != null ){
            dataSrcUrl = config.dataSrcUrl;
        }

        var jsonReaderConfig;
        if( config.jsonReaderConfig != null ){
            jsonReaderConfig = config.jsonReaderConfig;
        }

        var sortDirection;
        if( config.sortDirection != null ){
            sortDirection = config.sortDirection;
        }

        var sortField;
        if( config.sortField != null ){
            sortField = config.sortField;
        }

        var columnModelConfig;
        if( config.columnModelConfig != null ){
            columnModelConfig = config.columnModelConfig;
        }

        console.log( dataSrcUrl );
        console.log( sortDirection );
        console.log( sortField );
        console.log( CQ.Ext.util.JSON.decode( jsonReaderConfig ) );
        console.log( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        var proxy = new CQ.Ext.data.HttpProxy( {url:  dataSrcUrl} );

        reader = new CQ.Ext.data.JsonReader( {}, CQ.Ext.util.JSON.decode( jsonReaderConfig ) );

        var sourceStore = new CQ.Ext.data.Store(
            {
                proxy: proxy,
                sortInfo: {
                    field: sortField,
                    direction: sortDirection
                },
                reader: reader
            }
        );
        sourceStore.load();

        columnModel = new CQ.Ext.grid.ColumnModel( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        firstGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){

                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = firstGrid.getStore().getAt( index );

                    //Add the record to the destinationStore.
                    secondGrid.getStore().add( moveRecord );
                }
            },
            store            : sourceStore,
            name             : 'sourceGrid',
            colModel          : columnModel,
            stripeRows       : true,
            title            : 'Latest Articles',
            margins          : '0 2 0 0',
            x: 0,
            y:100,
            height: 500

        });

        //destination store
        var destinationStore = new CQ.Ext.data.Store({
            reader: reader
        });

        //destination grid
        secondGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){
                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = secondGrid.getStore().getAt( index );

                    //Remove the record from the destinationStore.
                    secondGrid.getStore().remove( moveRecord );
                }
            },
            name: 'destinationGrid',
            store: destinationStore,
            colModel: columnModel,
            stripeRows       : true,
            title            : 'Displayed Articles',
            enableDragDrop: true,
            hideHeaders: true,
            ddGroup: 'ddGroup',
            margins          : '0 0 0 3',
            x: 450,
            y: 100,
            height: 500
        });

        //Set the dropZone for the second grid so that users can manually re-order the list.
        secondGrid.on('render', function() {

            secondGrid.dropZone = new CQ.Ext.dd.DropZone(secondGrid.getView().scroller, {
                ddGroup: 'ddGroup',

                //If the mouse is over a grid row, return that node. This is
                //provided as the "target" parameter in all "onNodeXXXX" node event handling functions
                getTargetFromEvent: function(e) {
                    return e.getTarget(secondGrid.getView().rowSelector);
                },

                //On entry into a target node, highlight that node.
                onNodeEnter : function(target, dd, e, data){
                    CQ.Ext.fly(target).addClass('my-row-highlight-class');
                },

                //On exit from a target node, unhighlight that node.
                onNodeOut : function(target, dd, e, data){
                    CQ.Ext.fly(target).removeClass('my-row-highlight-class');
                },

                //While over a target node, return the default drop allowed class which
                //places a "tick" icon into the drag proxy.
                onNodeOver : function(target, dd, e, data){
                    return CQ.Ext.dd.DropZone.prototype.dropAllowed;
                },

                //On NodeDrop event
                onNodeDrop : function(target, dd, e, data){

                    var targetRowIndex = secondGrid.getView().findRowIndex(target);
                    var sourceRowIndex = data.rowIndex;

                    if( targetRowIndex == false ){
                        return false;
                    }

                    if( targetRowIndex == sourceRowIndex ){
                        return false;
                    }

                    var ds = secondGrid.getStore();

                    var sourceRecord = ds.getAt(sourceRowIndex);

                    ds.insert( targetRowIndex, sourceRecord );

                    //Reconfigure the destination list to new store.
                    secondGrid.reconfigure ( ds, columnModel );

                    return true;
                }
            });
        })

        //Tag Inputbox
        var tagInputField = new CQ.tagging.TagInputField({
            listeners:{
                addtag: function( inputField, tag ){

                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var tagsIndex = proxyUrl.indexOf( '?tags=' );

                    if( tagsIndex > -1 ){
                        proxyUrl = proxyUrl + "," + tagId;
                    } else {
                        proxyUrl = proxyUrl + "?tags=" + tagId;
                    }
                    //alert( "Add Tag: " + proxyUrl );
                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                },
                removetag: function( inputField, tag ){
                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var splitUrl = new Array();
                    splitUrl = proxyUrl.split( '?tags=' );

                    //alert( "Remove Tag 1 : " + proxyUrl );

                    if( splitUrl[1].indexOf( "," + tagId ) > -1 ){
                        splitUrl[1] = splitUrl[1].replace( "," + tagId, "" );
                    } else {
                        splitUrl[1] = splitUrl[1].replace( tagId , "" );
                    }

                    if( splitUrl[1].indexOf( ',' ) == 0 ){
                        splitUrl[1] = splitUrl[1].substring(1);
                    }

                    if( splitUrl[1] != null && splitUrl[1].length > 0 ){
                        proxyUrl = splitUrl[0] + '?tags=' + splitUrl[1];
                    } else {
                        proxyUrl = splitUrl[0];
                    }
                    //alert( "Remove Tag 2 : " + proxyUrl );

                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                }

            },
            title: 'Tag Properties',
            name: './tags',
            x: 250,
            y: 0,
            width: 400
        });
        CQ.form.GridtoGrid.superclass.constructor.call(this, config);

        this.selectionForm = new CQ.Ext.Panel({
            name         : this.name,
            width        : 'auto',
            height       : 600,
            border       : false,
            layout       : {
                type: 'absolute',
                padding: 0
            },
            items        : [
                tagInputField,
                firstGrid,
                secondGrid
            ]
        });
        this.add(this.selectionForm);

    },
    submitPanel: function() {

        var store = secondGrid.getStore();
        var storeData = store.data;
        var storeFields = store.fields;
        var params = {};

        if( storeData.length > 0 ){
            for(var i = 0; i < storeData.length; i++) {
                for(var j = 0; j < storeFields.length; j++){
                    var name = storeFields.item(j).name;
                    if (!params[name]) {
                        params[name] = new Array();
                    }

                    if( name == 'headline' ){
                        var headline = storeData.get(i).get(name);
                        if( headline.indexOf ( '_' ) == headline.length - 1 ){
                            params[name] = params[name].concat( storeData.get(i).get(name));
                        } else {
                            params[name] = params[name].concat( storeData.get(i).get(name) + "_");
                        }
                    } else {
                        params[name] = params[name].concat( storeData.get(i).get(name));
                    }
                }
            }
        }

        var serverResponse = CQ.utils.HTTP.post(this.urlPost, null, params, this);
        return CQ.utils.HTTP.isOk(serverResponse);
    },
    loadContent: function() {

        loadStore = null;

        if (!this.content) {
            var url = CQ.HTTP.externalize(this.urlGet);
            loadStore = new CQ.data.SlingStore({
                url: url+'.infinity.json'
            });
        } else if (this.content instanceof CQ.Ext.data.Store) {
            loadStore = this.content;
        }

        loadStore.load({
            callback: this.processRecords,
            scope: this
        });

    },
    processRecords: function(){

        var tagString = loadStore.getAt(0).get('tags');
        var sourceProxyUrl = firstGrid.getStore().proxy.url;

        if( tagString != undefined && sourceProxyUrl.indexOf( tagString ) == -1 ){
            sourceProxyUrl = sourceProxyUrl + "?tags=" + tagString;
        }
        //alert( "Process Records: " + sourceProxyUrl );
        var updatedSourceStore = new CQ.Ext.data.Store({
            url: sourceProxyUrl,
            reader: reader
        });

        updatedSourceStore.load();
        firstGrid.reconfigure( updatedSourceStore, columnModel );

        var tempHeadlineData = loadStore.getAt(0).get('headline');
        if( tempHeadlineData != undefined ){
            tempHeadlineData = "" + tempHeadlineData;
            var headlineData = new Array();
            headlineData = tempHeadlineData.split('_,');

            var tempPathData = loadStore.getAt(0).get('path');
            var tempDateData = loadStore.getAt(0).get('date');

            var secondGridData = [];

            for( var i = 0; i < headlineData.length; i++ ){
                var headline = headlineData[i].replace('_', '');
                secondGridData.push({headline: headline, path: tempPathData[i], date: tempDateData[i]});
            }

            //destination store
            var destinationStore = new CQ.Ext.data.Store({
                reader: reader
            });

            destinationStore.loadData( secondGridData );

            secondGrid.reconfigure( destinationStore, columnModel );
        }

    }
});
CQ.Ext.reg("gridtogrid", CQ.form.GridtoGrid);

2. Create a component in CQ and in the dialog add a widget node with the following properties.

Screen Shot 2013-10-04 at 12.18.59 PM

  • xtype : gridtogrid –> this matches to what you register the widget as in the javascript.
  • dataSrcUrl –> This is the URL which returns a JSON object that is consumed by the JSONReader in the widget JS
  • jsonReaderConfig –> Configuration for the JSON reader object
    • example: [{name: ‘headline’, mapping: ‘headline’},{name:’date’, mapping:’date’}]
  • columnModelConfig –> Configuration for the column model to be used in the left grid.
    • example: [{header: ‘Headline’, width: 500, sortable: false, dataIndex: ‘headline’},{header: ‘Date’, width: 200, sortable: false, dataIndex: ‘date’, hidden: true}]
  • sortField –> The name of the field that the data should be sorted on
  • sortDirection –> ASC/DESC
  • name –> name of property on the content node to store the destination list under. It will stored as a String[].

Screen Shot 2013-10-04 at 12.28.30 PM

3. Add the component on your page and double click to open the dialog. You should now see a list on the left and can populate the right based on the capabilities described above (double clicks).

Screen Shot 2013-10-04 at 12.39.13 PM

 

 

4. An example servlet that returns a JSON object to be consumed by the widget above is below

final ValueMap attributes = resource.adaptTo(ValueMap.class);
try{
TidyJSONWriter writer = new TidyJSONWriter(out);
String headline = "Headline Not Set";
Calendar date = null;
String stringDate = null;
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );
String path = null;

String tagsParameter = request.getParameter( "tags" );

if( tagsParameter != null ){
String[] tags = tagsParameter.split( "," );

NodeIterator articleList = Headlines.getArticleListByTags( currentNode.getSession(), tags, 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();

//Get Headline
try{
headline = article.getProperty( "headline" ).getValue().getString();
} catch (Exception e){
log.info( "headline doesn't exist, using static value instead" );
}

//Get Date
Node contentNode = currentNode.getSession().getNode( article.getPath().replace( "/contentpar/articleBody", "" ) );
try{
date = contentNode.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = contentNode.getProperty( "jcr:created" ).getValue().getDate();
}

//Convert date to String
if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = article.getPath().replace("/jcr:content/contentpar/articleBody","");
writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();
}
writer.endArray();
}

} else {
NodeIterator articleList = Headlines.getArticleListByModifiedDate( currentNode.getSession(), 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();
Node bodyNode = currentNode.getSession().getNode( article.getPath() + "/contentpar/articleBody" );

//Get Headline
try{
headline = bodyNode.getProperty("headline").getValue().getString();
} catch( Exception e ){
log.info( "headline doesn't exist, using static value instead" );
}

//Get lastModifiedDate
try{
date = article.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = article.getProperty( "jcr:created" ).getValue().getDate();
}

if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = bodyNode.getPath().replace("/jcr:content/contentpar/articleBody","");

writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();

}
writer.endArray();
}
}

} catch (Exception e){
log.info( e.getMessage() );
}

I can post a video of the widget in action if this blog post isn’t descriptive  enough. As always, please post comments/questions and I will try to get back to you as soon as possible.

12:46 PM Permalink

Customized multifield richtext editor – Part 2

Posted on Tuesday, September 17, 2013 By

This post is a continuation of (and final part) Customized multifield richtext editor 2 part series. In this post I will be covering the below mentioned customizations under Part 2 below.

Notes:

  1. I am using CRXDE Lite and CQ 5.6.1 for this article
  2. Save your work as often as you make ANY change.
  3. Please refer to “Configure the rich text editor” for more information on list of plugins and features available in CQ.
  4. Part 2 of this article is build upon common rte configuration that I created in Customized multifield richtext editor – Part 1.

 

List of Customizations:

I  divided this article into 2 part series. Below is the list of items that will be covered in each part.

PART -1 (these topics were covered in Customized multifield richtext editor – Part 1)

  1. Create a multi richText field,
  2. Common rte configuration – build once use often
  3. Customize default plugins
    • remove default plugins
    • restrict features in default plugins
  4. Activate a (non-default) Plugin,
    • adding your own special character,
    • Adding default (OOB) special characters back.

PART -2 (these are the things covered in this article)

5.   Use css styles in your rte (Using externalStyleSheet with rte),
6.   Add formatting,
7.   Make “plaintext” as default Paste Mode an strip extra HTML on paste.
8.   Remove default p tag in rte (limited scope)

5. Use css styles in your rte

Any style can be applied to a specific string that has been selected by the user in a rich text editor. Styles are based on CSS classes. When you select a string and apply a style to that it encloses the selected portion of text within span tags using the class attribute to reference the CSS class. Also, i am using a strikethrough as an example for this as there is no plugin that gives you this feature. For example:

<span class=strikethrough>Strikethrough Text Goes Here</span>

a.) Enable styles plugin (dropdown) for rich text editor

First thing that we need to do is enable the styles drop down for your rich text editor by activating the styles plugin. Lets do it:

1. Traverse to your rteconfig/rtePlugins node (/apps/blog/components/content/rteconfig/rtePlugins) and create a child node of type ‘nt:unstructured’ and name it ‘styles’.

2. Create a property called ‘features’ of type String under ‘styles’. 

3. To enable  features for the ‘styles’ enter the value as “*” for the ‘features’ property. Save.

Screen Shot 2013-08-27 at 2.09.11 PM

4. Now go back to your content page and refresh it. You should see a styles drop down from the ‘styles’ plugin. However, as there are no default styles it will be deactivated (empty/[None]).

Screen Shot 2013-08-27 at 2.11.03 PM

 

b.) Create css file and add styles to it

We also need a css file with required styes. These styles will be applied to the selected text when the author selects a string (text) in rich text editor and then selects a style from dropdown. Now we will create a css file and add required styles to it. I like to manage my css in client lib local to the component. We will walk through all the steps for creating the css, adding required styles and the reference it from the styles plugin. Lets start:

1. Traverse to your component node (/apps/blog/components/content/demo) and create a child node of type ‘cq:ClientLibraryFolder’ and name it ‘clientlib’.

Screen Shot 2013-08-27 at 2.32.22 PM

2. Create a folder under  ‘clientlib’ and name it ‘css’.

3. Create a file under ‘css’ folder and name it ‘static.css’

Screen Shot 2013-08-27 at 2.43.48 PM

 

4. Open your static.css and paste the below code in it. Save.

span.strikethrough {
    text-decoration:line-through;
}
#CQrte span.strikethrough {
    text-decoration:line-through;
}

Notes:
1. The span.strikethrough is for your content page. You would need to include your clientlib in your page template for this style to be applied.

2. #CQrte span.strikethrough will apply to the actual rich text editor in your component. Without this you will not see the strike through in your rich text editor.

c.) Referencing your style sheets.

In Part 1 of this article we created a fieldConfig under our component dialog at a path similar to /apps/blog/components/content/demo/dialog/items/items/title/items/title/fieldConfig.

1. Traverse to your components fieldConfig node and create a new property for fieldConfig with name as ‘externalStyleSheets’ and value as the path to the static.css that we created in last few steps .
Screen Shot 2013-08-27 at 3.07.09 PM

 

d.) Adding css style classes to ‘styles’ plugin dropdown.

Next we will create the required class under the styles plugin so that it is available in the ‘styles’ plugin dropdown.

1. Traverse to your rteconfig/rtePlugins/styles node (/apps/blog/components/content/rteconfig/rtePlugins/styles) and create a child node of type ‘cq:WidgetCollection’ and name it ‘styles’ (yes the name is same as parent node’s name).

2. Create a child node under this ‘styles’ node (from step 1 above – /apps/blog/components/content/rteconfig/rtePlugins/styles/styles): type of child node ‘nt:unstructured’ and name it ‘strikethrough’

Screen Shot 2013-08-27 at 3.14.46 PM

3. Create following properties for this ‘strikethrough’ node:

Property 1:

name: cssName

value: strikethrough

Property 2:

name: text

value: StrikeThrough

Screen Shot 2013-08-27 at 3.18.00 PM

4. Now go back to your content page and refresh it. Open the component and select a string in your rich text editor. Click on your styles plugin dropdown  in your rich text editor and you should see ‘StrikeThrough’ as one of the values available in your drop down list.

Screen Shot 2013-08-27 at 3.22.11 PM

5. A view of the rich text editor after StrikeThrough style applied to the ‘test’ text.

Screen Shot 2013-08-27 at 3.26.43 PM

 

6. Add Formatting

Now we will enable and customize the format options in rich text editor. Paragraph formats determine a type for the paragraph by assigning the correct block tag. The author can select and assign them using the Format selector. All text entered is placed within a block tag.

a.) Enabling the Format plugin

CQ has an OOB Format plugin the can be used to provide some basic formatting options. First we will look at how to enable this plugin and later we will customise it to add our own formats.

1. Traverse to your rteconfig/rtePlugins node (/apps/blog/components/content/rteconfig/rtePlugins) and create a child node of type ‘nt:unstructured’ and name it ‘paraformat’.

2. Create a property called ‘features’ of type String under ‘styles’. 

3. To enable  features for the ‘styles’ enter the value as “*” for the ‘features’ property. Save.

Screen Shot 2013-08-27 at 5.17.57 PM

4. Now go back to your content page and refresh it. You should see a Format drop down from the ‘paraformat’ plugin. The drop down for the Format has the default OOB options included in it. You can select text from your rte and the format will get applied to the entire block of text.

Screen Shot 2013-08-27 at 5.22.07 PM

b.) Customize the Format plugin to add other formats (h4)

Next we will add some paragraph formats to our Formats dropdown. I will be using h4 for my example.

Notes:

  • The default formats (<p>, <h1>, <h2> and <h3>) will be removed if you define custom formats (as specified above).
  • As <p> is the default format you must recreate this format.
  • The same applies to any formats that have already been used in existing content.

 

1. Traverse to your rteconfig/rtePlugins/paraformat node (/apps/blog/components/content/rteconfig/rtePlugins/paraformat) and create a child node of type ‘cq:WidgetCollection’ and name it ‘format’.

2. Create a child node under this ‘format‘ node (from step 1 above – /apps/blog/components/content/rteconfig/rtePlugins/paraformat/format): type of child node ‘nt:unstructured’ and name it ‘h4’.

3. To this node, add the property to define the block tag used.

Name: tag

Type: String

Value: h4

4. For same node add another property, for descriptive text to appear in the drop-down list.

Name: description

Type: String

Value: Heading 4

5. Now go back to your content page and refresh it. Although you cannot really see it but you should see a Format drop down with Heading 4 as one of the options in it. We will have to create another node for the p tag under the formats node (sibling of h4) before this works.

Screen Shot 2013-08-29 at 6.02.14 PM

 

Note: Repeat the steps 1 to 5 from above and create a p tag (description as Paragraph) and refresh your content page. You should be able to see Paragraph and Heading 4 as the 2 options now. Go ahead and select a text and apply heading 4 to test it.

Screen Shot 2013-08-29 at 6.09.54 PM

7. Make “plaintext” as default Paste Mode and strip extra HTML on paste.

The default paste mode in a rich text editor is “wordhtml”. Which means that when an author copy/pastes content into the rich text editor using Ctrl + V key or the main paste button the Word-compatible HTML pasting is done (this should suffice for most use cases, as it will keep most of the formatting, but removes unsuitable tags and attributes.

In my scenario the users wanted to change the default paste mode to “plaintext”  ( see below for a list of possible values) and strip and extra HTML tags which the author might have copied form the source.

Valid values are:

  • “browser” – use browser’s paste implementation (should usually not be used, as this may introduce unwanted markup or markup that could cause the RichText component to crash);
  • “plaintext” – for plain text inserts;
  • “wordhtml” – for Word-compatible HTML pasting (this should suffice for most use cases, as it will keep most of the formatting, but removes unsuitable tags and attributes.

The default copy and paste options are part of the ‘edit’ plugin in rich text editor and to over write the default paste option we need to include this plugin in our rteconfig/rtePlugins node as explained in Customized multifield richtext editor – Part 1.

Once you have the ‘edit’ plugin enabled with default options follow the below steps for making ‘plaintext’ as the default paste option and stripping off extra HTML tags.

1. Traverse to your rteconfig/rtePlugins/edit node (/apps/blog/components/content/rteconfig/rtePlugins/edit).

2. Create the blow list of properties for the edit node:

a. Create a property called ‘features’ of type String[] (multi) for ‘edit’ node. Leave the value as empty/blank for this property.

b. Create a property called ‘defaultPasteMode’ of type String for ‘edit’ node. Enter the value as ‘plaintext‘ for this property.

c. Create a property called ‘stripHtmlTags’ of type Boolean for ‘edit’ node. Enter the value as ‘true‘ for this property.

Screen Shot 2013-09-09 at 3.29.58 PM

Note: The “crazy” test that I am showing below might not be a real world scenario but I just want to show the working code.

Open Any website and copy some come (with formatting) content from the webpage as in below screen shot. Now Copy this formatted text.

Screen Shot 2013-09-09 at 3.48.07 PM

Paste this content into your rich text editor and  you should see the results of formatting being removed form your pasted text.

Screen Shot 2013-09-09 at 3.50.10 PM

 

8. Remove default p tag in rte (limited scope)

Note: I have removed the Format plugin for purpose of my demonstration in this topic.

Not sure if you have noticed but when an author types in anything inside the rich text editor (even if you are in the HTML Editor mode) it gets encapsulated by a P tag. I came across a situation in which the requirement was to get rid of the default p tag so the rendering engine consuming the content does not have to parse it out. Please note that you will still get a bunch of P tags when you have a line break. This is how I got it working.

1. Traverse to your components ‘fieldConfig’ (/apps/blog/components/content/demo/dialog/items/items/title/items/title/fieldConfig) node and create a new property for ‘fieldConfig’ with name as ‘removeSingleParagraphContainter’, type as Boolean and value as true. Save.

Screen Shot 2013-09-09 at 4.29.10 PM

2.  No go back to your content page and refresh it. Add a single line of text while in RichText mode and then toggle between SourceEdit mode and Rich Text mode. You should not see the P tags wrapping your line of content.

Screen Shot 2013-09-09 at 4.56.15 PM

 

Enjoy!

 

 

 

 

1:45 PM Permalink