Sunday, September 26, 2010

Using webDAV and Cadaver with dotCMS

As a developer, one of the most annoying things about dotCMS is how many darn *clicks* it takes to edit anything. There must be an easier way to access files in dotCMS. There is: webDAV and Cadaver. Both of these allow limited access to dotCMS's files: the former through Windows Explorer, the latter through a command line.

Connect to the dotCMS Using webDAV

Look at the Connect to the dotCMS Using webDAV page (for Windows, Mac and Linux). This lets you see webDAV files through your file explorer (Windows Explorer on Windows). But be warned: you can't edit these files in place. Instead, you have to make copies of them, edit them locally, then click and drag the files back to the webDAV directory, replacing the originals. You can bookmark the directories though. Also, you don't see everything - you can't see .dot files for example.

Using autopub vs nonpub 

The URL you use to connect with webDAV can control whether changes you make are automatically published or not. See the below examples.

  • http://example.com/webdav/autopub means files are published as they are uploaded.
  • http://example.com/webdav/nonpub means files uploaded as "working" copies.

Cadaver

Macabre name, but a great tool for command line access to webDAV collections such as dotCMS. It is a *nux tool, but there is Cadaver for Cygwin too! Use it like you use the BSD ftp command line tool - when connected, type ? for a list of the commands you can use.

First thing to do is set up a .netrc file that contains your webDAV credentials.

machine     example.com
login       your-username
password    you-password

Then you can connect to it with the following command:

cadaver -t http://example.com/webdav/autopub/dir/you/want

Replace /dir/you/want with whatever directory path you want to start in. Also note the differences between using /webdav/autopub and /webdav/nonpub as noted above in the Using autopub vs nonpub section.

Transferring files via Cadaver script

Of course, what is life without a script? Here is a script that will make transferring files a lot easier.

#! /bin/bash
# webDavTransfer.sh
if [ "$1" != "-d" ] ; then
   echo Usage $0 -d dest-path files ...
   echo Transfer files from current directory to dest-path in webDAV.
   exit 2
fi
destPath=$2
shift; shift

cadaver -t <<EOF
open http://example.com/webdav/autopub/base-path
cd $destPath
mput $@
quit
EOF

The pre-requisite to using this script is that you have ~.netrc file set up as described above, also copied below.

machine     example.com
login       your-username
password    you-password

This is how to use the script:

cd path/to/local/files
webDavTransfer.sh -d tests/rob *.vtl

The result should be what looks like FTP output, and your files should be transferred.

Don't forget that aliases can make it even easier: 

alias myTransfer='webDavTransfer.sh -d tests/rob'

Then all you need to run is:

myTransfer *.vtl

So you can make an alias for each path you transfer to.

Pages that helped me write this post.

My dotCMS notes.

Using cdargs in Cygwin

The cdargs utility is a very powerful directory bookmarking in *nix shells, including Cygwin. There are many wonderful pages on how to use and extend cdargs, some of which I reference at the end of this post.

Unfortunately, these pages talk about some utility functions (mark, cdb, cv, ca) that you get from sourcing cdargs-bash.sh - which I don't have in my copy of Cygwin. After reading man cdargs though, I found that the two most important functions were easy to create for myself: mark to create a cdargs bookmark and cdb to cd into the directory referenced by the bookmark.

Put these in your .bashrc or .bash_profile.

# cd to a cdargs bookmark.
function mark() {
   if [ $# -ne 1 ] ; then
     echo Usage: mark cdargs-bookmark-name
     return 2
   fi
   cdargs --add=:$1:` cygpath -u -a .`
}

# Create a bookmarks in cdargs for the current directory.
function cdb() {
   if [ $# -ne 1 ] ; then
     echo Usage: cdb cdargs-bookmark-name
     return 2
   fi
   cdargs "$1" && cd "`cat "$HOME/.cdargsresult"`" ;
}

Using them is easy enough too. Use mark to create a cdargs entry (stored in the plain text file ~/.cdargs - which you can even edit with a text editor). Then use mark to go to (cd) a bookmarked directory. If you ever need to be reminded what your bookmarks are, look at the ~/.cdargs file or just run cdargs.

Example usage.

cd /c/really/long/path/that/is/hard/to/type/and/remember
mark bigPath

Now you have bookmarked the directory against the key bigPath. To get back to that directory easily, use the command cdb bigPath.

The following pages have helped me learn more about how to use cdargs.

Search for dotCMS users by group or role

You interact with dotCMS users in Velocity via the $cmsuser object, which is an instance of the com.dotmarketing.viewtools.CMSUsersWebAPI class and provides various methods for looking up user objects.

The searchUsersAndUsersProxy method lets you search for users by name, group name or role. Below is the code I use to search for all users who have the Administrator role and then output Name (email address) for each of them. I have only tested this on dotCMS 1.7.

#set($found = $cmsuser.searchUsersAndUsersProxy(null, null, null, [], false, ["Administrator"], true, null, 1, 1000))
<p>Found $found.get("users").size() Administrator users.</p>
#set($theUsers = $found.get("users"))
#foreach ($user in $theUsers)
   $user.fullName ($user.emailAddress)<br>
#end

Below are some important notes about this code.

  • This method is the only one in the class designed for pagination i.e. for displaying results one page at a time. The last two parameters are page - as in what "what page of results do you want to return?" - and pageSize - as in "how many results should be on this page?"
  • The limit argument for this method (pageSize) has slightly different semantics to the limit argument in other search techniques such as # results in the pullContent macro. In the latter, 0 means no limit on the results, but in searchUsersAndUsersProxy it really means 0! If you intend on pulling back all results in one page, use a number so big that you will never get that many users.

    At the time of writing, Facebook has more than 500,000,000 users, Twitter has more than 75,000,000 users, Hotmail has more than 360,000,000 users and Gmail has more than 170,000 users. I think 1,000 is well and truly more than the total number of Administrator users the site I am working on will ever have. A dotCMS JIRA bug was raised for this discrepancy and I posted a question to StackOverflow when I was trying to work why my original code wasn't working.
  • The fourth parameter, groupNames should be empty square brackets [] - which is an empty array/ArrayList for Velocity; which does not support the keyword null (my first guess as a Java programmer).
  • The fifth parameter, showUserGroups should be false. If true, you will get lots of duplicates (as you would if there was a full join between selecting users matching role X and any group).

I found this useful when I was working on a dotCMS form handler whose intention was to allow users of the site to submit feedback for review by the site administrators. I used the above technique to extract all of the administrator email addresses as recipients for the email (addresses which you will find are not displayed in the HTML source, if you read the post I linked to). I came across the issues mentioned above and tried to solve them in this post to the dotCMS Yahoo group: Getting email addresses of all users with the Administrator role.

For testing purposes, here is the SQL that will do the same job (get email addresses for every user with an Administrator role).

select user_.emailaddress
from user_
INNER JOIN users_roles
ON users_roles.userid = user_.userid
INNER JOIN role_
ON users_roles.roleid = role_.roleid
where role_.name = 'Administrator';

Pages that helped me write this post.

My dotCMS notes.

Thursday, September 23, 2010

dotCMS form handler as a jQuery dialog with encrypted email

dotCMS 1.7 has fairly straight-forward form handling that makes it easy to write (in HTML) a form that will submit to dotCMS (action="/dotCMS/submitWebForm") and email the form contents to an email address you nominate. The purpose of this posting is to show the following.

  1. How to create a simple dotCMS form using this mechanism that emails the results to someone.
  2. How to encrypt the email addresses so that they do not appear in plain text within your HTML
  3. How to use some simple jQuery to make the form a "floating dialog" that can be triggered by clicking on a button.

Since I don't have a dotCMS server of my own, here are screen shots of the results.

This is what the form looks like.

Click the button and the form is displayed. It isn't a pop-up (those are evil); it's a hidden element that gets shown (less evil) when you click "Give Feedback". After the user has filled out the form and clicks "Submit", they are redirected back to the same page.

And this is the email you get from dotCMS containing details of the submitted form.

Next I will show the code in chunks, with a bit of discussion for each chunk.

The first things you need are imports for the jQuery theme, jQuery API itself and then jQuery UI.

<link href='http://robertmarkbram.appspot.com/content/global/css/jQueryUi/customThemeGreen/jquery-ui-1.8.4.custom.css' rel='stylesheet' type='text/css'/>
<script type="text/javascript" src="http://www.google.com/jsapi?key=ABQIAAAAoDEIY_vXge_LQOEVgHyheBSvIISGg2D4cAMKlpvPZkPgQSL0sRRGyBerqkXeyllTDvkGdlqzeYPWKA" ></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.min.js"></script>

What's with the jsapi?key and the imports from ajax.googleapis.com? Google (being the thoroughly non-evil company that they are) offer several well known Javascript APIs for public use on websites other than their own i.e. they host them so you don't have to. They ask that you register for (free) and use an API key specific to your site so they know ... whatever it is they want to know. Read more about this on the Google Libraries API - Developer's Guide. This is perfect for things like Blogger i.e. services for which you cannot upload your own scripts or other arbitrary files.

Here is the HTML for the visible portion of the page.

<div style="border:1px solid;padding:10px;margin:10px;width:200px;">
   <p>My very interesting page.. </p>
   <button id="opener">Give Feedback</button>
</div>

This is the HTML for the dialog that will appear when the user clicks the button.

<form method="post" id="feedbackForm" name="feedbackForm" action="/dotCMS/submitWebForm" style="display:none;">
   <input type="hidden" name="from" id="from" value="noreply@example.com">
   #encrypt('robertmarkbram@gmail.com') #set ($to = $encryptedStr)
   <input type="hidden" name="to" id="to" value="$to"/>
   <input type="hidden" name="subject" id="subject" value="Feedback on your cool site"/>
   <input type="hidden" name="returnUrl" id="returnUrl" value=""/>
   <input type="hidden" name="errorURL" id="errorURL" value="/error/page" />
   <input type="hidden" name="html" id="html" value="1" />
   <input type="hidden" name="order" id="order" value="referrer,reason,feedback" />
   <input type="hidden" name="formType" id="formType" value="feedbackForm" />
   <fieldset>
      <h1>Feedback Form</h1>
      <p>Please use this form to provide feeback.</p>
      <input type="hidden" id="referrer" name="Referrer URL" value="" />
      <p>Reason for feeback</p>
      <select id="reason" name="Reason for Feedback">
         <option value="Your site is awesome!">Your site is awesome!</option>
         <option value="Are you my father?">Are you my father?</option>
      </select>
      <p>Comments</p>
      <textarea id="feedback" name="Feedback"></textarea>
      <p><input type="submit" value="Submit" /></p>
   </fieldset>
</form>

Notes about this code.

This form submits to /dotCMS/submitWebForm - which is the dotCMS code that will process the data and email it out to whoever is specified in the hidden to field.

It is very easy to encypt the email address that will appear in the hidden to field. Do it with the code below.

#encrypt('robertmarkbram@gmail.com') #set ($to = $encryptedStr)
<input type="hidden" name="to" id="to" value="$to"/>

This is how the HTML looks on the browser:

<input type="hidden" value="aJ8pVupCRaDYMRzM8qXtCS4IGXAInnAtgKPynQZNDe4=" id="to" name="to">

dotCMS's /dotCMS/submitWebForm automagically decrypts it to get the actual email address(es).

The hidden returnUrl field specifies what URL the browser will go to after the form is submitted (shown below). The jQuery discussed further down sets this value.

<input type="hidden" name="returnUrl" id="returnUrl" value=""/>

This jQuery Javascript sets up the dialog and sets up a button to open it. Plus it initialises data for the form - specifically what URL this form is hosted on.

<script language="javascript">
  $(document).ready(function() {
     // Creates a dialog from element with ID "feedbackForm"
    var $dialog = $("#feedbackForm").dialog({
      autoOpen: false,
      title: 'Feedback Form',
      height: "auto",
      width: "auto"
    });
      // If you click the button (ID "opener") it will open the form dialog.
    $('#opener').click(function() {
      $dialog.dialog('open');
      // prevent the default action, e.g., following a link
      return false;
    });
    // Redirect back to the same page after the form is submitted.
      $("#returnUrl").val(window.location.href);
    // To inform email recipient of the URL the form was from.
      $("#referrer").val(window.location.href);
  });
</script>

Note that the hidden returnUrl field is given the value of window.location.href - so after clicking the button to submit the form, the browser will go back to the same page (the dialog form will not be visible again though; just like what happens if you refresh the page).

For a more detailed code example showing what options you can specify in the form, see the dotCMS Wiki page on Sample Form Code. See the dotCMS documentation page, Form Handling for an explanation of the different options. For example, you can even provide an alternate email template.

Current Form Handling. In dotCMS 1.9, there is a new way to build forms that means you don't need knowledge of HTML and makes the forms more re-usable as dotCMS Form entities. See the new Form Handling documentation for instructions on this.

I am not sure if the approach outlined in this post still works in 1.9. I am also not sure if the new approach hides emails. Or how easy it is to apply the jQuery touch.

My dotCMS notes.

Sunday, September 19, 2010

jQuery Slideshow

Presented below is a jQuery slideshow, with the code discussed below that. Originally, this was based on Rodrigo Silveira's Javascript Prototype Slideshow, re-written for jQuery. But in Chrome I was witnessing a disturbing flicker (that not all Chrome users were seeing) and alternative code was suggested by Peter Ajtai in my StackOverflow post, Why is this slideshow flickering? This post now uses and presents Peter's adaptation of my re-write of Rodrigo's code. And they say there is nothing new in the world?

Here is a standalone version without the chaff from before the write, and here is a standalone version without the chaff showing the current code.


Here is the code.

First up, you need to import the jQuery API.

<script type="text/javascript" src="http://www.google.com/jsapi?key=ABQIAAAAoDEIY_vXge_LQOEVgHyheBSvIISGg2D4cAMKlpvPZkPgQSL0sRRGyBerqkXeyllTDvkGdlqzeYPWKA" ></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>

What's with the jsapi?key and the imports from ajax.googleapis.com? Google (being the thoroughly non-evil company that they are) offer several well known Javascript APIs for public use on websites other than their own i.e. they host them so you don't have to. They ask that you register for (free) and use an API key specific to your site so they know ... whatever it is they want to know. Read more about this on the Google Libraries API - Developer's Guide. This is perfect for things like Blogger i.e. services for which you cannot upload your own scripts or other arbitrary files.

Next is the Javascript, written against jQuery.

<script type="text/javascript">
   // Contain all your functionality in a self calling anonymous
   // function, so that you don't clutter the global namespace.
   (function() {
      // ------
      // ###### Edit these.
      // Assumes you have images in path named 1.jpg, 2.jpg etc.
      var imagePath = "images";
      var lastImage = 5;         // How many images do you have?
      var fadeTime = 4000;       // Time between image fadeouts.

      // ------
      // ###### Don't edit beyond this point.
      // No need for outer index var
      function slideShow(index) {
         var url = imagePath + "/" + index + ".jpg";
         // Add new image behind current image
         $("#slideShow").prepend($("<img/>").attr("src",url));
         // Fade the current image, then in the call back
         // remove the image and call the next image.
         $("#slideShow img:last").fadeOut("slow", function() {
            $(this).remove();
            setTimeout(function() {
               slideShow((index % lastImage) + 1)
            }, fadeTime);
         });
      }
      $(document).ready(function() {
         // Img 1 is already showing, so we call 2
         setTimeout(function() { slideShow(2); }, fadeTime);
      });
   })();
</script>

Note that we are creating the second image within this Javascript ($("#slideShow").prepend($("").attr("src",url));), so the HTML only needs to define one image at the start.

Here is the CSS needed to render two images on top of each other.

<style>
   #slideShow {
      position:relative;
      width: 500px;
      height: 500px;
   }
   #slideShow IMG {
      position:absolute;
   }
</style>

And lastly, the HTML - you need to fill in the first image yourself.

<div id="slideShow">
   <img id="slideShowBack"  src="images/1.jpg" />
</div>

One last important note (at least to this post). Part of the reason for the flickering I witnessed in Chrome was attributed to the re-rendering of some fancy border related CSS on the IMG elements. The fix was to remove the CSS from the IMG and put it on a surrounding DIV instead. Since the CSS is coming from my Blogger template, I had to first "reverse" the template's CSS, and then add it to the surrounding DIV. Below is the CSS I used to do just that.

/* Overriding Blog styles to put IMG border on the surrounding DIV. */
.post-body IMG {
   padding: 0px;
   background: white;
   border: none;
   -moz-box-shadow: none;
   -webkit-box-shadow: none;
   box-shadow: none;
   -moz-border-radius: 0;
   -webkit-border-radius: 0;
   border-radius: 0;
}
.post-body #slideShow {
   padding: 8px;
   background: #ffffff;
   border: 1px solid #cccccc;
   -moz-box-shadow: 0 0 20px rgba(0, 0, 0, .2);
   -webkit-box-shadow: 0 0 20px rgba(0, 0, 0, .2);
   box-shadow: 0 0 20px rgba(0, 0, 0, .2);
   -moz-border-radius: 0;
   -webkit-border-radius: 0;
   border-radius: 0;
}

Saturday, September 18, 2010

Event Handling with the jQuery Autocomplete Combobox

Updates.
Tuesday 5 April 2011, 02:18:59 PM - added code that copies TITLE across from the original SELECT to the new INPUT.

First, the exciting bit. Have a look at the list of Star Trek characters below. See if you can guess which actor plays your favourite character - before you make a selection. Each time you make a selection, you will see the actor's name appear in the text box beside the combobox.


Now for the really cool bit: try typing TNG into the combobox. See what happens? The list is shortened to include only those options that begin with "TNG". Now try typing Spock into the combobox. The list is shortened to include only those options that contain "Spock" (not just begin with).

This is a demonstration of jQuery's combobox UI widget (links to demo page) - currently described as a prototype - which is built on top of the autocomplete UI widget.

The autocomplete feature has an overwhelming appeal, meaning that I couldn't bear to use any other jQuery combobox after I saw this one in action; but there is a cost. This widget replaces a SELECT control with a text field, a button and a floating list, thus it is a complicated task to make them all work as one; and it isn't done perfectly (yet).

  • Event handling is incomplete and not compliant to jQuery naming standards (though the latter is minor and will be easy to fix).
  • The floating list needs adjustment to be made scrollable (or large lists will be unmanageable).
  • The combobox doesn't adjust to fit the width of its contents like a HTML SELECT control should.
  • You need a whole jQuery theme just to get the autocomplete control (the floating list coming down from the text field) to render decently - plus extra CSS fixes specifically for the combobox, i.e. it's a lot of effort and CSS if all you want is one (very cool) combobox control. Granted, if you know your CSS well enough you don't need a whole jQuery theme; but after looking at what goes into a theme, I didn't want to spend the time to pick out what pieces are needed - plus I wanted to be ready for when I might use other jQuery themed controls.

Below is my adaptation of jQuery's combobox UI widget. I present the code in chunks and explain what I have changed from the original. There is also a standalone version of this example that shows the control without the chaff.

The first things you need are imports for the jQuery theme, jQuery API itself and then jQuery UI.

<link href='http://robertmarkbram.appspot.com/content/global/css/jQueryUi/customThemeGreen/jquery-ui-1.8.4.custom.css' rel='stylesheet' type='text/css'/>
<script type="text/javascript" src="http://www.google.com/jsapi?key=ABQIAAAAoDEIY_vXge_LQOEVgHyheBSvIISGg2D4cAMKlpvPZkPgQSL0sRRGyBerqkXeyllTDvkGdlqzeYPWKA" ></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.min.js"></script>

What's with the jsapi?key and the imports from ajax.googleapis.com? Google (being the thoroughly non-evil company that they are) offer several well known Javascript APIs for public use on websites other than their own i.e. they host them so you don't have to. They ask that you register for (free) and use an API key specific to your site so they know ... whatever it is they want to know. Read more about this on the Google Libraries API - Developer's Guide. This is perfect for things like Blogger i.e. services for which you cannot upload your own scripts or other arbitrary files.

Next is the CSS specifically for the combobox.

<style type="text/css">
   .ui-button { margin-left: -1px; }
   .ui-button-icon-only .ui-button-text { padding: 0.35em; }
   .ui-autocomplete-input { margin: 0; padding: 0.48em 0 0.47em 0.45em; }
   .ui-autocomplete { height: 200px; overflow-y: scroll; overflow-x: hidden;}
</style>

I added the last line to make sure the autocomplete portion would scroll on long lists - which would otherwise be unmanageable. I copied the code from this jQuery forum post: Autocomplete with vertical scrollbar. I am still not entirely happy with some aspects of the above CSS. It renders nicely on Firefox 3.6.8, Internet Explorer 8 and Chrome 6 - but the edges didn't line up correctly in Chrome 7 (Dev build): the text field seemed just a pixel or two taller than the button. Worse, zooming in and out in all of those browsers could also make the heights mis-match by just a pixel or so.

Below is the actual code for the jQuery autocomplete combobox widget. Of course, this belongs in its own javascript file unless you plan to use it one page only.

<script type="text/javascript">
   (function( $ ) {
      $.widget( "ui.combobox", {
         _create: function() {
            var self = this;
            var select = this.element,
               theWidth = select.width(),
               selected = select.children( ":selected" ),
               theTitle = select.attr("title"),
               value = selected.val() ? selected.text() : "";
            select.hide();
            var input = $( "<input style=\"width:" + theWidth + "px\">" )
               .val( value )
               .attr('title', '' + theTitle + '')
               .autocomplete({
                  delay: 0,
                  minLength: 0,
                  source: function( request, response ) {
                     var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
                     response( select.children( "option" ).map(function() {
                        var text = $( this ).text();
                        if ( this.value && ( !request.term || matcher.test(text) ) )
                           return {
                              label: text.replace(
                                 new RegExp(
                                    "(?![^&;]+;)(?!<[^<>]*)(" +
                                    $.ui.autocomplete.escapeRegex(request.term) +
                                    ")(?![^<>]*>)(?![^&;]+;)", "gi"
                                 ), "<strong>$1</strong>" ),
                              value: text,
                              option: this
                           };
                     }) );
                  },
                  select: function( event, ui ) {
                     ui.item.option.selected = true;
                     //select.val( ui.item.option.value );
                     self._trigger( "selected", event, {
                        item: ui.item.option
                     });
                  },
                  change: function( event, ui ) {
                     if ( !ui.item ) {
                        var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ),
                           valid = false;
                        select.children( "option" ).each(function() {
                           if ( this.value.match( matcher ) ) {
                              this.selected = valid = true;
                              return false;
                           }
                        });
                        if ( !valid ) {
                           // remove invalid value, as it didn't match anything
                           $( this ).val( "" );
                           select.val( "" );
                           return false;
                        }
                     }
                  }
               })
               .addClass( "ui-widget ui-widget-content ui-corner-left" );
            var span = $("<span style=\" white-space: nowrap;\"></span>")
                  .append(input).insertAfter( select );
            input.data( "autocomplete" )._renderItem = function( ul, item ) {
               return $( "<li></li>" )
                  .data( "item.autocomplete", item )
                  .append( "<a>" + item.label + "</a>" )
                  .appendTo( ul );
            };

            $( "<button> </button>" )
               .attr( "tabIndex", -1 )
               .attr( "title", "Show All Items" )
               .insertAfter( input )
               .button({
                  icons: {
                     primary: "ui-icon-triangle-1-s"
                  },
                  text: false
               })
               .removeClass( "ui-corner-all" )
               .addClass( "ui-corner-right ui-button-icon" )
               .click(function() {
                  // close if already visible
                  if ( input.autocomplete( "widget" ).is( ":visible" ) ) {
                     input.autocomplete( "close" );
                     return;
                  }

                  // pass empty string as value to search for, displaying all results
                  input.autocomplete( "search", "" );
                  input.focus();
               });
         }
      });
   })(jQuery);
</script>

I changed parts of the original to make the combobox re-adjust to match the size of the underlying SELECT control. The original and modified versions are shown below for comparison.

// Original
var self = this;
var select = this.element.hide(),
 selected = select.children( ":selected" ),
 theTitle = select.attr("title"),
 value = selected.val() ? selected.text() : "";
var input = $( "" )
.attr('title', '' + theTitle + '')

// Modified. Can you see Wally?
var self = this;
var select = this.element,
   theWidth = select.width(),
   selected = select.children( ":selected" ),
   value = selected.val() ? selected.text() : "";
select.hide();
var input = $( "<input style=\"width:" + theWidth +
.val( value )
.attr('title', '' + theTitle + '')

Although the width of the combobox should now match the width of the underlying SELECT, the font used in the jQuery theme can still screw up the effect. E.g. a comparatively large (or small) font will still make the combobox appear to be the wrong width (the HTML SELECT would just re-size itself). I also made sure to copy over TITLE from the SELECT over to the INPUT that is being made here.

Another change I made from the original was to ensure that the two elements (an INPUT and a BUTTON) are enclosed by a SPAN with white-space: nowrap to ensure that the controls always stay together and never wrap

// Original
var input = $( "<input>" )
.insertAfter( select )
.val( value )

// Modified, Part 1. Removed insertAfter.
var input = $( "<input>" )
.val( value )

// Modified, Part 2, added two lines after .addClass( "ui-widget...
.addClass( "ui-widget ui-widget-content ui-corner-left" );
var span = $("<span style=\" white-space: nowrap;\"></span>")
      .append(input).insertAfter( select );

That's the end of the "infrastructure" code. Below is an abridged version of the underlying SELECT, an input that will display the actor's name when a selection is made (to demonstrate event handling), and a button that will toggle display of the underlying SELECT control.

<select id="starTrekCharacters">
   <option value="William Shatner">TOS - Captain James T. Kirk</option>
   <!-- et etc -->
 </select>
<input type="text" id="starTrekActor" length="30"/>
<button id="toggle">Show underlying select</button>

For your own combobox, you only need the SELECT. The INPUT and BUTTON are just there to help demonstrate event handling and to toggle showing the underlying SELECT control.

Also note that the jQuery combobox will drop any OPTIONs that don't have a value; so if you have a blank OTPION at the start (like my -- Choose a Star Trek character. --) you must give it a value and make sure that any back-end system knows how to deal with that value too.

The jQuery code that turns a SELECT into a combobox is also very easy, as seen below.

<script language="javascript" type="text/javascript">
   // This is run when the document has loaded.
   $(document).ready(function() {
      // Create a combobox
      $("#starTrekCharacters").combobox({
         // And supply the "selected" event handler at the same time.
         selected: function(event, ui) {
            $("#starTrekActor").val($("#starTrekCharacters").val());
         }
      });
      // NOT NEEDED for combobox; included just to let you see underlying SELECT.
      $("#toggle").click(function() {
         $("#starTrekCharacters").toggle();
      });
   });
</script>

As you can see, the code to create a combobox from a SELECT and provide an event handler is almost trivial.

$("#starTrekCharacters").combobox({
   selected: function(event, ui) {
      // ...
   }
});

Unfortunately, the documentation for the base autocomplete widget doesn't mention combobox and I had some difficulty working out how I was meant to adapt the SELECT event handler that is documented (see below).

$( ".selector" ).autocomplete({
   select: function(event, ui) { ... }
});

The main problem is that the jQuery standard event name for something being selected is select, but combobox calls it selected. Also, the documentation for autocomplete mentions six events - but combobox only handles one. This is all understandable since combobox is still a prototype. It's just hard to work out if you don't know what to look for - as described in this jQuery forum post Using Events in Combobox, in this StackOverflow post, Hooking event handlers to jQuery Autocomplete Combobox, in this comment on Jörn Zaefferer's blog post A jQuery UI Combobox: Under the hood and this rejected bug report: Autocomplete fails to call event handler for combobox.

So that's it for my long winded introduction to jQuery's autocomplete combobox widget. Despite the time I have spent working out how to use this - and the fact that it is still a prototype - the autocomplete functionality has me hooked and I will continue to use it wherever I can.

Wednesday, September 15, 2010

UltraEdit Function List shows function names missing last letter

Sometimes the UltraEdit Function List shows the function names missing the last letter - of every function. I have noticed this a few times. The fix is to make sure the file is in Unicode format: File > Conversions > ASCII to Unicode.

Saturday, September 11, 2010

UltraEdit macro for outputting anchor tags in HTML

One of the most common (and therefore painful) HTML elements to write repeatedly is an anchor tag. There is so much detail to fill in. First you have to write the A tag itself, opening and closing it. Then you have to copy and paste the URL from your browser into an HREF attribute, then you have to cursor into the middle of it and think of something descriptive to write for the display text. It's just so much hard work!

<a href="http://robertmarkbramprogrammer.blogspot.com">my blog</a>

I do most of my coding "by hand" in the wonderful and amazing UltraEdit which is extensible through, among other means, a macro language. Here is my macro for outputting anchor tags. I promise that this macro will make you truly enjoy coding HTML again. In fact, I had two weeks annual leave coming up which I was going to spend on the beach with my partner: but I cancelled it just so that I could spend the time hand coding the HTML for more blog posts! That's right - this macro really is that good.

Use it by first copying the URL you need; then type your display text, select it and run this macro: it will correctly output <a href="copied url">selected display text</a>| and leave the cursor where the pipe character is.

InsertMode
ColumnModeOff
HexOff
IfSel
Clipboard 9
Cut
"<a href=""
Clipboard 0
Paste
"">"
Clipboard 9
Paste
Clipboard 0
"</a>"
Else
"<a href=""
Paste
""></a>"
Key LEFT ARROW
Key LEFT ARROW
Key LEFT ARROW
Key LEFT ARROW
EndIf

Alternatively, you can use it without typing and selecting the display text: copy the URL you need and then run this macro: it will output <a href="copied url">|</a> and leave the cursor where the pipe character is.

How to escape quotes in Velocity

Jan 14, 2011. Thanks to Michael Fienen's comment below, quoting Strings in Velocity is not so hard; just use ${esc.q}.

#set($searchTerm = "red house" )
#pullContent("+structureInode:6333 +live:true +text1:${esc.q}$searchTerm${esc.q}" '0' 'random')

You can ignore the rest of this post - I leave it here purely for the sake of posterity. :)


Short story: skip to the bit about how to escape quotes in Velocity.

In dotCMS, I use Velocity to perform various Lucene searches through macros such as #pullContent, for which there is a well defined Lucene Query Parser Syntax. A simple example: find all content belonging to some structure called "Historic Buildings" whose name includes the token "house". Let's say it has an inode value of 6333 and and it's name field (a.k.a. the "variable" column in a Structure) is text1 (a.k.a. "index name" column in a Structure). Below is a query to find all Historic Buildings whose name includes the token "house" - in random order.

#set($searchTerm = "house" )
#pullContent("+structureInode:6333 +live:true +text1:$searchTerm" '0' 'random')

That's easy enough, and it will find all values that have "house" anywhere in their value (as a whole word). A phrase search is required when you want to search for an unbroken string, for example you want to search for "red house" and match "the big red house" but not match "red (recently painted) house". In Lucene, a phrase search is easy: just contain the string in double quotes: "red house"

How do you escape quotes in a Velocity string? My first thought was to use backslash as an escape character - which is common to many languages.

#set($searchTerm = "\"red house\"" )
#pullContent("+structureInode:6333 +live:true +text1:$searchTerm" '0' 'random')

Unfortunately Velocity doesn't recognise the backslash as an escape character and you will get a parser error from this. As I learned from this Apache mailing list response: escape double quotes it seems that escaping quotes in Velocity is very ugly. Here is what seems to me to be the most robust method. 

#set($searchTerm = "red house" )
#set($Q = '"')
#set($quotedTerm = "${Q}$searchTerm${Q}")
#pullContent("+structureInode:6333 +live:true +text1:$quotedTerm" '0' 'random')

Btw, a very good way to build your Lucene query strings is to use the dotCMS Admin interface. When you use it to search for content, click the "Show Query" button at the bottom left (below the search results and other buttons) and you will see the query string for the search just completed.

My dotCMS notes.

When to use single or double quotes in Velocity

Did you see that episode of the Simpsons where Bart writes on the blackboard: "I shall not use single quotes around variable names in Velocity." No? *Sigh.* Neither did I, which is why I write this post. It is my penance given in the hope that I will not make this mistake a fourth time, or at least recognise the mistake immediately by, say, the tenth time.

In Velocity, single and double quotes are similar to Bash scripting: variable references will be evaluated in double quotes, but not single quotes. For example, if you have #set( $name = "Rob" ) then "My name is $name" will output My name is Rob but 'My name is $name' will output My name is $name.

I think the best practice is to use double quotes by default and use single quotes only when you specifically want to avoid evaluating the contents of the string.

"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."
"I shall not use single quotes around variable names in Velocity."

Read more about the quoting rules and more in Apache's User Guide for Velocity.

My dotCMS notes.

Thursday, September 09, 2010

dotCMS combo-box from pullContent macro

In dotCMS, it's very easy to build a combo-box (a.k.a. SELECT or drop-down) from content drawn from the #pullContent() macro. Below is how to do it.

## Build a combo-box from content of a given Structure.
## $limit - Max number of results (0 for all results)
## $structure - inode of the structure
## $sortField - Results will be sorted by this field.
#macro( writeSelectAnyStructure $structure $sortField $limit)
   #pullContent("+type:content +live:true +structureInode:$structure" "$limit" "$sortField")
   <select id="qt$structureInode" name="qt$structureInode">
      <option value=""></option>
      #foreach($content in $list)
         #if($UtilMethods.isSet($!content.title))
            <option value="$!content.inode">$!content.title</option>
         #else
            <option value="$!content.inode">$!content.name</option>
         #end
      #end
   </select>
#end

My dotCMS notes.

dotCMS pull related content for transitive relationships

In dotCMS, it's easy enough to use the #pullRelatedContent() macro traverse a relationship to get a list of, say, all books related to author. But what if you wanted to traverse a chain of relationships? Let's say you have the following pair of transitive relationships: Author-Book and Book-Publisher, you have an Author and need to find what Publishers are related to her?

Here is how you do it. First, you pull related content from one relationship. Then you iterate through the results, pulling related content from the second relationship - merging the results into a separate list. You should then sort the list at the end.

## Author has books. Book has publisher.
#set($publishers = [])
#pullRelatedContent("Author-Book" "$authorInode" "0" "name")
#set($bookList = $list)
#foreach($book in $bookList)
   #pullRelatedContent("Book-Publisher" "$book.inode" "0" "title")
   $publishers.addAll($list)
#end
#set($list = $publishers)
#set($list = $sorter.sort($publishers, "name"))

After this, you might need to remove the duplicates.

Here is the post I made to the dotCMS Yahoo group about this: Compound relationship query.

My dotCMS notes.

dotCMS: paginate results from pulling related contents

Pull Related Content

In dotCMS, the #pullRelatedContent() macro is used to create a list of all content related to some base content through a named relationship.

Syntax for the call is as below. The nice thing about it is that the call doesn't care if your content inode refers to a child or parent to the relationship. For example, let's say you have a bunch of books and a bunch of authors linked through a relationship called book_authors. In the call below, content inode could the inode of a book or an author.

#pullRelatedContent("relationship name" "content inode" "limit" "sort by")

After the call, you will have a $list variable available that references a java.util.ArrayList with your results.

Paginating Content

When you have a query that you know will return a large set of results, you should paginate the results. This means showing only a portion of the results at a time with controls that let user go to the next/previous page or even click on a specific page number within the results. In dotCMS, the #pageContent() does this.

#pageContent("query" "sort by" "# per page" "# current page")

Actually, all it does is process your query and leave you with the following variables: list, totalResults and totalPages. You then output the actual pagination controls for yourself based on this information.

Paginating Related Content

Both #pageContent() and #pullRelatedContent() take query as a parameter and leave you with a $list - which means you cannot call #pageContent() on the results of #pullRelatedContent() (e.g. #pageContent(#pullRelatedContent(...), ...).

So how do you paginate related content?

The answer is that you need to use a relationship query in #pageContent(), providing the identity (not inode of the content whose related items you wish to show. I do this with my own macro, as below.

#macro ( paginateRelatedContent $relationshipName $baseInode )
   #set($x = 0)
   #set($x = $webapi.getIdentifierInode($baseInode))
   #if($x < 1)
      #set($x = $inode)
   #end
   #set($uQuery = "+type:content +live:true +deleted:false +${relationshipName}:${x}")
   #pageContent("$uQuery" "text1" "3" "$page")
#end

Syntax is below.

#paginateRelatedContent("name of a relationship" "content inode")

Here is my post about this on the dotCMS Yahoo group: Paginate related content?

My dotCMS notes.