Add a Button to the Sidekick in AEM

24. February 2014

I wanted to conditionally add a button to the Adobe Experience Manager (AEM) Sidekick in Author edit mode.  The examples I found, seemed geared toward CQ5 and were not working in AEM 5.6.

After some more investigation I was able to piece together the following sample code:

CQ.WCM.getSidekick().getBottomToolbar().insertButton(0, 
  (new CQ.Ext.Button({
    "iconCls": "cq-sidekick-miscadmin",
    "tooltip": {
      "title": CQ.I18n.getMessage("Custom"),
      "text": CQ.I18n.getMessage("Custom action from AEM sidekick"),
      "autoHide": false
    },
    "handler": function() {
      alert('doing action');
      console.log(this); /*verify ‘this’*/
    },
    "scope": CQ.WCM.getSidekick() /*sidekick made sense to me, but could be anything*/
  }))
);
CQ.WCM.getSidekick().getBottomToolbar().doLayout();

The benefit of this is that you don't have to influence the sidekick for the entire AEM instance, but instead enable an action for a specific page/template.

While this adds a button, there are other locations on the Sidekick to target, such as custom actions. 

AEM

Effects of Uninstalling a Package in AEM

11. February 2014

Adobe Experience Manager (AEM formerly CQ5) offers a feature to install and uninstall packages of content and functionality.

A package is a zip file with files and meta data to write and/or overwrite nodes in the JCR.  You upload the package to AEM using the API or the CRX Package Manager. Then you install the package and AEM will unzip the contents and write them to the JCR.  If a package is installed, you have the option to uninstall it, which raises the question, what is restored when the package is uninstalled.

I put together a few tests to exercise this and document the results.

First, I created a package and only targeted a single location in the JCR.

Here is the filter xml file:

<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
    <filter root="/etc/clientlibs/packagetest"/>
</workspaceFilter>

The filter XML file is located here in the package.

<package-root>\META-INF\vault\filter.xml

Next, I created some files using CRXDE Lite at the packagetest location to create a baseline.  Then I ran some tests and here are the results.

Test 1

  • Install the package
  • Observe that everything in the packagetest node has be deleted and replaced with the package contents
  • Uninstall the package
  • Observe that the original contents of packagetest have been restored

 

Test 2

  • Install the package
  • Observe that everything in the packagetest node has be deleted and replaced with the package contents
  • Edit a file under packagetest
  • Add a new file under packagetest
  • Uninstall the package
  • Observe that all changes made after the package install have been reverted, the package has been removed and the original contents of packagetest have been restored

 

Test 3

  • Install the package
  • Observe that everything in the packagetest node has be deleted and replaced with the package contents
  • Download the package (don't change the name)
  • Modify a file in the package zip
  • Upload the package (check box to force override)
  • Reinstall the package
  • Observe that the file change is present
  • Uninstall the package
  • Observe that not only is the file change gone but the packagetest node is reverted to the state prior to the initial install

 

In conclusion, here are some best practices to keep you out of trouble.

  1. Don't use CRXDE Lite to modify files on your QA or Production instances
  2. Alway version your packages so you can roll back to a previous version if something goes wrong
  3. Make sure your filters narrowly define what nodes your package can influence

AEM

Component Group Doesn't Display in AEM Sidekick

2. December 2013

If you are using Adobe Experience Manager (AEM, CQ5.6) you know that you can organize the components that appear in the WYSIWYG editor's "Sidekick" into groups.  You simply add a "componentGroup" attribute to the component node. However, if the Sidekick has too many items, your component group may not appear.

If your component group is not appearing in the Sidekick and instead your component is appearing under "Other", you are probably bumping into one of the following constraints: (See http://forums.adobe.com/message/5029192 )

1) There is a max number of groups allowed (default = 4), and the rest will be assigned to the "Other" group to create 5 total groups

2) If the max group count is met, any component group having fewer than 3 components selected, will instead be combined into the "Other" group. This can lead to a visible group count of less than five, so that only those groups having 3 or more components and the "Other" group will appear.

3) The number of components selected per group will take precedence, so groups with the most selected components will be displayed and groups with fewer components will be combined into the "Other" group.

UPDATED: Feb 10, 2014

Your component group will not appear in the design mode parsys component selector dialog, if it starts with a lower case letter. So to be safe, always start your component group name with a capital letter.

AEM

How To Install Orchard CMS on GoDaddy Shared Hosting

17. November 2013

I was trying to use Orchard CMS in the shared hosting environment at GoDaddy.  However, once I published my site, the only thing that displayed was this cryptic message,

"The page cannot be displayed because an internal server error has occurred."

I had custom errors off, like so, but that did not help display a more clear error.

<customErrors mode="Off" />

This is not the standard ASP.NET error screen so the problem must be before ASP.NET.

  • It could be a typo in the config file which is making it malformed
  • It could be that the host environment does not match the .NET compilation requirements (4.0+ as of this writing)
  • It could be a permissions problem

I was pretty confident my web.config was without typos, because it worked on my local dev machine.

I ensured the site was set to use IIS7 .NET 4/4.5.

I made sure the App_Data directory was marked as writable, because I was planning to use SQL Server Compact edition and I noticed that Orchard stored some site settings in the App_Data/Sites directory.

However, none of these fixed the issue.

Then I stumbled across this article on the Orchard Site. It mentioned that Orchard CMS required Full trust.  The article mentions that Orchard is configured out of the box to run as Full trust, so I figured that setting was already in the web config.  Well, it wasn't.

I added Full trust under the system.web node in the web.config.

<trust level="Full" originUrl="" />

I reloaded the home page of the site and after churning for a while, the site finally came up.

Other advice recommended giving everything Read/Write permissions, but that seemed ridiculous.  I did have to add Read/Write permission to the Media directory so that images could be uploaded via the CMS authoring system.  For now, I think it is all working fine.

OrchardCMS, Programming

Modify string prototype to select DOM elements with jQuery

27. June 2013

I should probably say "Use with Caution" because you really shouldn't mess with the prototype of built-in types.  However, I have found this useful and am putting it here for personal reference.

In certain scenarios I was often getting the ID of some DOM element from server generated code or elsewhere and it was not formatted as a DOM query that jQuery could use. 

As a result I got tired of writing code like this:

$('#'+elementId).show();

I guess it was all the shifts and plusses that bugged me, so I wrote this:

String.prototype.$=(function($) {
    return function(prefix,suffix){
      var s=this.trim(),pre=(prefix||'#');
      if (s.length===0) return $();
      return $(((s.indexOf(pre[0])===0)?s:pre[0].concat(s))+(suffix?' '+suffix:''));
    };
  })(jQuery);

Now you can get the jQuery element(s) like this:

var showElement = function(elementId) {
    elementId.$().show();
};

If you don't want it to be by id or want to add other qualifiers you could do this:

var showElement = function(elementId) {
    elementId.$('.', '> li').show();
};

JavaScript, jQuery

Configuring the AEM Title Editor for In-place Editing

17. June 2013

If you want to use the Adobe Experience Manager (AEM, formally CQ5) in-place or in-line editing feature you can run into some pitfalls.  

First, the one that it took me a while to track down, was a JavaScript error whenever I attempted to edit the text inline.  

The JavaScript error was this:

"No title container found (must be a 'h1' tag)"

The component I made was not rendering an h1 tag anywhere in the output of the jsp, so the inline editor did not know what text should be edited.  However, I did not want to use an h1 tag around my content.

Second, I was frustrated that it seemed I had no control over what property the in-place editor would use when saving the modified value.  It always chose "./jcr:title" or "./text".

If you look at the documentation at dev.day.com, there is a short blurb about the cq:inplaceEditing node.  However, it is the "config" node that is the key to customizing the inline editing for your needs.  I could not find information about the "config" node, so I thought I would put it up here.

Location of the Config Node

The Adobe documentation for the cq:inplaceEditing node of type cq:InplaceEditingConfig mentions the "configPath" property.  This is not the path to a common cq:Dialog panel or the path to the persistence property as I thought.  It is the path to a node that has properties for configuring the client-side in-place editing.  You can use the path to point to a common configuration node.  The other option is to just ignore this property and instead create the "config" node as a child of the cq:inplaceEditing node.  This is the default location for the node.

- my_component
  `-- cq:editConfig
       `-- cq:inplaceEditing
            `-- config

Properties of the Config Node

I tracked down the default property values from the /libs/cq/ui/widgets.js file.

"textPropertyName": "./jcr:title",
"titleTag": "h1",
"imageReplacingTag": "h3",
"imageReplacingStyle": "margin-top: 0;",
"imageTitleAttribute": "alt",
"enterKeyMode": "save"

The same names that are used here may also be used in CRXDE Lite to setup the config node.  Simply create a property using the name and enter a value that reflects the change you want.  You only need to add the properties you wish to modify, otherwise the default value is used.

NOTE: I found in geometrixx that the titleTag was a String[] (array / Multi) property.  At this point, I don't know if any of the others are a special type.  I haven't had the need to change them.

Fixing the JavaScript Error

My component was emitting an h2 tag with a nested span tag, so I changed the titleTag to "span".  This fixed the JavaScript error because the client-side code was now looking for the correct HTML tag within the context of my component and was finding it fine.

Changing the Persistance Property

I wanted to store the text of my component into a property named "./title" so I added the textPropertyName property with a value of "./title"

NOTE: The cq:inplaceEditing node can have an editorType property with one of these values: title, text, or plaintext. If you choose the editorType of "title" then the default persistence property is "./jcr:title", but if you choose "text" then the default persistence property is "./text".  The config allows you to choose the "text" or "plaintext" option depending on the HTML support you need while still controlling the field used for persistence.

AEM ,

Unofficial Launch.co "Enhance Tickers" Bookmarklet

27. December 2012

Bookmarklet: Enhance Tickers

(Installation: In most browsers you can install the bookmarklet (above) by dragging it to the bookmarks bar in your browser. In IE you have to right-click on the link and choose "Add to Favorites", making sure to add it to the "Favorites" folder.)

What does it do?

Launch.co uses the "$ABC" or "Cashtags" convention for identifying companies. However, if you don't recognize a ticker symbol, it can be difficult to comprehend the cryptic news blurbs. This is where the bookmarklet helps.

First, bring up the Launch page and then click the bookmarklet. It will convert all the stock symbols into Google search links which open in a new window. Then in the background it will load the company name, stock price and daily change values to display when your mouse hovers over a ticker symbol. The stock data will reload every 5 minutes and new CMS entries will be enhanced every 20 seconds as they are created by the authors.

Since Launch.co is a single page app, you will not need to re-run the bookmarklet unless you have reloaded the entire page using the refresh button in your browser or something similar.

To "Uninstall" the enhanced features, simply click the refresh button in your browser or use one of the alternate keyboard commands like F5 or Cntrl-R.

Updates

12/27/2012 - This first release does not link to an external js file.  All the code is in the bookmarklet (except: jQuery).  I think I will release a version that uses an external file so that it can be improved without re-installing. Future ideas include adding the P/E ratio to hover title and also adding the Twitter name to all @ mentions.

11/17/2013 - The Launch.co site no longer uses "Cash Tags" so this bookmarklet is somewhat irrelevant now.  I'll keep the source code up in case it is helpful.

No Warranty

THE PROGRAM IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY WARRANTY. IT IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW THE AUTHOR WILL BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

Novelties, Tools ,

Starter Template for Creating a jQuery Plugin

18. January 2012

Every time I needed to create a jQuery plugin, I would debate with myself on how to structure the code. It occurred to me that it would be helpful to stub out a sample plugin template that I could grab and jump right into coding.

After looking around at various jQuery plugin patterns, I came up with the template at the end of this post. I think it has a nice structure, but would enjoy hearing any suggestions. The one thing I may add to this in the future is the ability to programmatically uninstall the plugin.

Here are some samples of how you might invoke the plugin and then call it's API.

// Install the plugin on all matching elements
$("div.myPlugin").myPlugin();

// Install the plugin and override a default setting
$("div.myPlugin").myPlugin({
    playPauseSelector:'#playbutton'
});

// Control multiple instances at once 
//  and also maintain the fluent API using
//  the jQuery plugin function to call the API
$("div.myPlugin").myPlugin("play", 2); 
$("div.myPlugin").myPlugin("play", 2).myPlugin("pause"); 

// Control a single instance by accessing the attached API
// Only the officially supported API methods can be called
$("div.myPlugin").get(0).MyPlugin.play(2); 

Here is the code:

NOTE: Some parts of the code are included only to illustrate aspects of the structure. See the code comments for more explanation.

(function ($) {

    // Create the plugin jQuery function
    $.fn.myPlugin = function (optionsOrCommand, param) {

        // Setup the default options
        var defaultOptions = {
            playPauseSelector: '.MyPluginPlayPauseButton',
            playingClass: 'playing',
            pausedClass: 'paused'
        };
        
        // Define a function to setup the plugin instance
        var create = function (root, options) {
 
            // Grab a reference to the root DOM element that is the target of the plugin
            var _root = $(root);
            var _playPauseButton = null;

            // Update the settings with any overrides
            var _opts = $.extend({}, defaultOptions, (options || {}));
            
            // Create internal helper methods
            var _play = function(idx) {
                _playPauseButton.removeClass(pausedClass).addClass(playingClass);
            };
            
            var _pause = function() {
                _playPauseButton.removeClass(playingClass).addClass(pausedClass);
            };
            
            // Define an init function to kick off the plugin functionality
            var _init = function () {
                _playPauseButton = $(playPauseSelector, _root);
                _playPauseButton.bind('click', function() {
                    var self = $(this);
                    if (self.hasClass(playingClass)) {
                        _pause();
                    } else {
                        _play();
                    }
                });
            };
            
            // Return an object containing a reference to the API and the init method
            // We don't want the init in the API so it is separated here
            return {
                controller: {
                    init: function () {
                        _init();
                    }
                },
                api: {
                    pause: function () {
                        _pause();
                    },
                    play: function (idx) {
                        _play(idx);
                    }
                }
            };
        };
        
        // Cycle through each matching element and return them  
        // so as not to break the jQuery fluent API
        return this.each(function () {

            // Grab a reference to the root element
            var root = this;

            // Determine whether or not the plugin is already installed
            var exists = !(root.MyPlugin === undefined || root.MyPlugin === null);

            // Initialize the optionsOrCommand so we can handle either input
            if (optionsOrCommand === undefined || optionsOrCommand === null) {
                optionsOrCommand = {};
            }

            // If the optionsOrCommand is a string then map it to the API
            if (optionsOrCommand.constructor == String) {
                if (exists) {
                    switch (optionsOrCommand) {
                        case 'pause':
                        case 'play':
                            if (typeof root.MyPlugin[optionsOrCommand] === 'function') {
                                    root.MyPlugin[optionsOrCommand](param);
                            }
                            break;
                        default:
                            return;
                    }
                }
                return;
            }

            // If the plugin is not installed then create it and apply the API
            // to the DOM element.
            if (!exists) {
                var plugin = create(root, optionsOrCommand);
                root.MyPlugin = plugin.api;
                plugin.controller.init();
            }
        });
    };
})(jQuery);

Here are some links to other interesting plugin patterns:
JQUERY GENERATOR BY Kees C. Bakker
Creating a jQuery plugin
Keeping the plugin and jQuery separate

jQuery, Programming , ,

A jQuery action plugin to insert a custom function into the method chain

19. May 2010

I was hanging out at the #jquery IRC Channel through freenode Web IRC and a user wanted to have a callback function on the removeClass method.  This seems like a strange request since the removeClass method is a synchronous call and returns the list of jQuery items which were manipulated.

The problem could be solved as follows:

var items = $(".target").removeClass("target");
doSomething();
items.addClass("newTarget");

However the user was not satisfied with this option, so I suggested adding a callback to the removeClass method like so:

(function($) {
  var oldRemoveClass = $.fn.removeClass;
  $.fn.removeClass = function(value, callback) {
    var ret = oldRemoveClass.call(this, value);
    if ($.isFunction(callback)) {
      callback.call(ret);
    }
    return ret;
  }
})(jQuery);

NOTE: A sample is available on JS Bin.

However, after some consideration, I thought of a another option (again, of questionable value, but hey...) Why not add a jQuery function that takes a callback and returns the fluent context. The code would be quite similar to the above but would apply more broadly.

Plugin code

/*Final plugin*/
(function($) {
  $.fn.action = function(callback) {
    var ret = this;
    if ($.isFunction(callback)) {
      callback.call(ret);
    }
    return ret;
  }
})(jQuery);

Now with the new function in place the user can string a custom function into the method chain. The original solution would be refactored to look like this.

$(".target").removeClass("target").action(doSomething).addClass("newTarget");

The action plugin could allow the callback to filter or replace the item context, but I think that would hurt readability and lead to hidden bugs.

jQuery, Programming , , ,

Moved blog from Blogger to BlogEngine.NET

19. May 2010

I managed my old blog with Blogger but the FTP deployment process was kind-of-a pain. Google agreed and stopped supporting it.  At the time, I was going to switch to a .NET blog solution, but I didn't love any of the options out there and was too lazy to copy over the old posts manually.  Additionally, Twitter was gaining momentum and seemed to fit with the limited amount I had to say.

After a while I started having ideas for longer posts and wanted to put them on the blog, but I felt I had to upgrade first.  Needless to say that didn't happen. 

When BlogEngine.Net came along I wanted to import my posts with their BlogML import helper but for some reason I couldn't get it working. Recently, I tried it again and the current version (1.6.1) worked like a charm.  I have all my old posts and I am ready to put up some content.

I'd like to thank Aaron Lerch for his PowerShell script which I used to extract my BlogML from Blogger.

Script: BloggerToBlogML.ps1
Website: http://www.aaronlerch.com/blog/
License: Public Domain

The PowerShell script relies on the blogML .NET library which can be downloaded from Codeplex.

Programming , , ,