Search This Blog

Friday 17 May 2013

On click select all text in a form field

Problem

In some scenarios a form user may need to go through a form replacing values in the fields. If the user has a preference for using the keyboard and tabbing to the field then this is easy as the contents of the field is selected, when the user starts typing they overwrite the value. 
If your users have a preference for using the mouse then the behaviour is a bit different. When the user clicks into a field they go into insert mode at the point where they click. To replace the contents of the fields they can delete all the characters and start typing, or press Ctrl-A (to select the fields contents) and then start typing, or select the contents of the field with the mouse and start typing. In this situation it would be easier for the user if clicking into a field selected the contents.

Solution

The solution here was to add some code to the click event of the fields to replicate the behaviour of the tabbing though the form.

Detailed explanation

The code in the click event is;

xfa.host.setFocus(SelectOnClick.somExpression);
app.setTimeOut("xfa.host.setFocus('"+this.somExpression+"');",50);

This code sets the focus to the field SelectOnClick then 50 milliseconds later sets the focus back to the current field, setting focus this way causes the contents of the field to be selected, just like tabbing to it.

For this to work the field SelectOnClick must be set to Visible (Print Only), which means it can still receive focus but is not visible to the user. Then in the prePrint event set the field to hidden and in the postPrint event set the field to visible so the user won't see it on paper either.

This sample, OnClickSelect.pdf, shows a normal field, a field with the above click event code and a field that selects all text on a double click.


Wednesday 15 May 2013

JavaScript Objects in XFA Forms

Problem

JavaScript objects allow you to reuse functionality across your forms but there are a some tricks to getting them to work in the Adobe Acrobat / Reader environment.

Solution

There a a couple of ways to create objects in JavaScript. Using the new operator and a constructor function, or using the object literal syntax. The new operator has some advantages as it allows the developer to enforce an initialisation script be executed and is easily understood by programmers coming from languages that support classes. Using the literal syntax might require a static method to create the object or an 'initialise' method ( or 'start' method in the sample below ). But using the new operator requires a work around that is well described in John Brinkman's blog, http://blogs.adobe.com/formfeed/2009/09/script_objects_deep_dive.html. This is an alternative that might fit some situations and uses the literal syntax to create JavaScript objects defined within an XFA script object.

Detailed explanation

This sample defines an object that can be used for measuring elapsed time, a StopWatch, and can be used like;

var sw1 = StopWatch.stopWatch("StopWatch1");
sw1.start();
for (var i=0; i<1000; i++) {} // or something we want to measure
sw1.stop();
sw1.println();

This will output a message on the console that looks like;

StopWatch1:Time elapsed 127ms

Or using a static method to construct the object and achieve the same result;

var sw1 = StopWatch.startNew("StopWatch1");
for (var i=0; i<1000; i++) {} // or something we want to measure

sw1.stop();
sw1.println();


The script object is called StopWatch and the function defined within it is called stopWatch, but note that we do not use the new operator to create it. The stopWatch function returns a inner object that retains access to the variables and functions of stopWatch without exposing them to the calling code. This is called a closure and more information about closures and nested functions is available at https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope/#Nested_functions_and_closures.

This means there is no way outside code can access the variables startTime, elapsed, isRunning or the function getElapsedTime.


form1.#variables[0].StopWatch - (JavaScript, client)
function stopWatch(label){
    /**
     *  Private variables used internally by the StopWatch object.
     */
    var startTime = 0;
    var elapsed = 0;
    var isRunning = false;
 

    /**
     *  A private helper function to return the number of milliseconds since the
     *  StopWatch was started.
     */
    function getElapsedTime()
    {
        return Date.now() - startTime;
    }
  

 
    /**
     *  The StopWatch object as visible to other form code.
     */
    return {
        /**
         *  Determines if a StopWatch is running
         *  @return {boolean} true is the StopWatch is running otherwise false.
         */
        getIsRunning: function IsRunning()
        {
            return isRunning;
        },
        /*
         * Returns the total time since the StopWatch was first started.
         * @return {int} The total number of elapsed milliseconds.
         */
        getElapsed: function getElapsed()
        {
            var result = elapsed;
            if (isRunning)
            {
                result += getElapsedTime();
            }
            return result;
        },
        /*
         * Starts (or continues), measuring elapsed time.
         */
        start: function start()
        {
            if (!isRunning)
            {
                isRunning = true;
                startTime = Date.now();
            }
        },
        /*
         * Stops (or pauses) measuring elapsed time.
         */
        stop: function stop()
        {
            if (isRunning)
            {
                elapsed += getElapsedTime();
                isRunning = false;
            }
        },
        /*
         * Stops the StopWatch and resets the elapsed time to zero.
         */
        reset:function reset()
        {
            elapsed = 0;
            isRunning = false;
            startTime = 0;
        },
        /*
         * Outputs the elapsed time to the console.
         */
        println: function println()
        {
            console.println(util.printf("%sTime elapsed %,0dms",
                                        (label === undefined) ? "" : label + ":",
                                        this.getElapsed()));
        }
    }
}


This approach would allow me to pass in an argument to stopWatch and have different objects returned, perhaps one measuring in decimal time, or more usefully in a real example having different objects returned depending on a country id passed in that provided appropriate calculations, validations and initialisations. Creating objects this way is known as the factory pattern.

I have often used the StopWatch code when experimenting with individual parts of my form that are not performing fast enough. But I have also modified John Brinkman's trace macro http://blogs.adobe.com/formfeed/2010/06/add_trace_to_form_script.html to add code to all script events in a form. More details of macros and how to install them can be found in one of his earlier blogs, http://blogs.adobe.com/formfeed/2010/01/designer_es2_macros.html, but it should be as simple as creating a directory C:\Program Files (x86)\Adobe\Adobe LiveCycle Designer ES2\Scripts\AddExecutionTimes, copy the AddExecutionTimes.js file to the folder and restart Designer. Once restarted there will be an option under Tools ... Scripts called AddExecutionTimes.js. If there is any problem running the macro then the messages should be displayed on the Log tab of the Report window.

Make a copy of the form before running this macro as I don't have a macro to remove the StopWatch code that gets inserted. Now when you preview your form messages will appear with the form object somExpression, the event name and the elapsed time, something like;

xfa[0].form[0].form1[0].#subform[0].TextField1[0].#calculate:Time elapsed 92ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[0].DecimalField1[0].#calculate:Time elapsed 58ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[1].DecimalField1[0].#calculate:Time elapsed 60ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[2].DecimalField1[0].#calculate:Time elapsed 60ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[3].DecimalField1[0].#calculate:Time elapsed 92ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[4].DecimalField1[0].#calculate:Time elapsed 86ms
xfa[0].form[0].form1[0].#subform[0].TextField1[0].#validate:Time elapsed 57ms
xfa[0].form[0].form1[0].#subform[0].Button1[0].event__click:Time elapsed 2,565ms


The StopWatch object has been modelled after the .net system.diagnostics.stopwatch object that I have found useful, but that one uses performance counters, this one uses the JavaScript date object so all timings are just indicative, you may have to run your tests a number of times to get a more accurate measure and minimise the number of other processes running on your machine. But hopefully this will point to parts of your form that are chewing up the most time and help identify candidates for optimisation.

StopWatch.pdf is a sample with the macro already run, when opened the console will be displayed with initialisation messages displayed, as you interact with the form more messages will be displayed.

StopWatch.xdp a script fragment containing the StopWatch object.

AddExecutionTimes.js the macro for adding StopWatch code to all of your events.

Thursday 9 May 2013

Date handling in Livecycle Designer ES forms

Problem

The internal format of a DateTimeField is yyyy-mm-dd or yyyy-mm-ddThh:mm:ss. These are sensible formats but formats that the standard JavaScript Date object does not handle in its constructor nor does it have methods to display a date into these formats. There are also several common date operations that are not handled by the JavaScript Date object, methods like addWeeks, getWeekOfYear, getAge, etc.

Solution

This sample shows how to extend the standard JavaScript object and add handling for new date formats in the constructor as well as add new methods to format dates, perform date arithmetic, and return additional information about a date. All the normal JavaScript Date object methods are still available and the object can still be used were a JavaScript Date object can be used like the util.scand function.

Detailed explanation

The constructor function of this new object can take a string with a date in the yyyy-mm-dd or yyyy-mm-ddThh:mm:ss formats or just take a DateTimeField object itself.

So to test for a valid date in the exit event you can use the following code;

if (isNaN(XFADate.newDate(this)))
{
    // handle invalid date
}

Using isNaN (is not a number) is a standard way of testing if a JavaScript Date object has a valid date value.
Below is a screen shot of the sample form using the XFADate script object;


Other things to note about Date handling in forms.


Selecting a date with the date picker


One of the nice things about the standard date picker popup that might not be widely known,  the first date picker gives you a month view, click the heading "April 2012" (in this sample) and you get a year view, click the year and you get a decade view and finally click the decade header and you get a century view.


Entering yyyy-mm-dd format dates


One of the not so great features of the DateTimeField is that it will always allows the user to type a date in the yyyy-mm-dd format, regardless of the display or edit formats.  This has not been a problem for me, but the user can also type yyyy-mm and the DateTimeField will default to the 1st of the month, even though the rawValue will retain the yyyy-mm format.  To some this will be a handy short cut but to others will cause confusion.

To make sure the displayed value of a DateTimeField matches the rawValue when the date value has a yyyy-mm format in the exit event I have the following code to update the display and edit formats;

if (isNaN(XFADate.newDate(this)))
{
    this.format.picture.value = "date{'"+this.rawValue+"'}";
    this.ui.picture.value = "date{'"+this.rawValue+"'}";
}

To make this work I need to save the specified display and edit formats in the initialize event by adding nodes under the field's desc element;

this.desc.nodes.append(xfa.form.createNode("text", "displayFormat"));
this.desc.nodes.namedItem("displayFormat").value = this.format.picture.value;
this.desc.nodes.append(xfa.form.createNode("text", "editFormat"));
this.desc.nodes.namedItem("editFormat").value = this.ui.picture.value;


In the enter event I set the display and edit formats to their defaults (if these are invalid then the calendar picker will not open).

this.format.picture.value = this.desc.nodes.namedItem("displayFormat").value;
this.ui.picture.value = this.desc.nodes.namedItem("editFormat").value;


util.scand


Another quirk of date handling in the Acrobat API is the util.scand method, this allows a string to be parsed into a date, like util.scand("yyyy-mm-dd", "2012-04-02"). The problem that occurs is the Date object returned will have a random amount of milliseconds set.  This generally isn't a problem but if you are comparing dates a few milliseconds can make a big difference.  The second page of the sample form demonstrates this problem.  The XFADate script object resolves this problem by using a regex to parse the date string.

This function also has the same problem as the DateFileField when handling dates without a day, so util.scand("yyyy-mm-dd", "2012-04") will return a JavaScript Date object with a value of 2012-04-01.

Also, if a null value is passed into util.scand then then a Date object with the value set to the current date will be returned.  This can easily become a problem with a call like util.scand("yyyy-mm-dd", Date1.rawValue), if Date1.rawValue is null then the current date is returned.

Download the XFADateForm.pdf or the script object fragment XFADate.xdp.


Monday 6 May 2013

Double Click event in LiveCycle Designer forms

Problem

Double Click is often used as a way to select a default option, but none of the controls supplied with Designer support a double click event.

Solution

This solution uses an app.setTimeOut() method in the click event to re-execute the click event in 250ms, if within that time there is another click from the user then it is taken as a double click, otherwise the single click will occur when the click event is re-executed.

Detailed explanation

This code goes in the click event of the control

var clickTime = new Date().getTime() - XFAUtil.getProperty(this, "clickTime");
if (clickTime < 250) //double click time less than 150ms{
    // Double click code goes here

    app.clearTimeOut(app.timeout);
    XFAUtil.deleteProperty(this, "clickTime");
}
else
{
    if (xfa.event.fullText !== "singleClick")
    {
        app.timeout = app.setTimeOut("xfa.event.fullText='singleClick';xfa.resolveNode('"+this.somExpression+"').execEvent('click');",250);
        XFAUtil.setProperty(this, "clickTime", new Date().getTime(), XFAUtil.ContentType.Time);
    }
    else
    {
    // Single click code goes here    }
}


XFAUtil is my script object to store and read values in the <desc> element of a field or the <variables> element of a subform.

At the first click the clickTime will be greater than 250ms (as XFAUtil.getProperty(this, "clickTime") will return a null).  The xfa.event.fullText property will also not equal "singleClick" as this is my flag to say this is a re-executing click and is set as part of the app.setTimeOut().

My first example is a List Picker control, an item can be selected by clicking item then the Select or Unselect button or you can double click an item.



Another example is a Portlet style, an item can be selected by double clicking it or from the context menu which is displayed on a single click.


Download sample, DoubleClick.pdf.

Sunday 5 May 2013

Drop-Down List Control with auto-complete (Searchable Drop-Down List)

Note:

An updated version of this sample is available in a later blog here.

 

Problem

In the standard drop-down list control that comes with LiveCycle Designer when a user types a character the first item starting with that character is selected. But in other environments it is becoming more common to have full auto-completion, so as the user types matches include the second, third, etc characters.

Solution

This solution uses a textbox as the drop-down list UI component and a list box to display the selected items. This is a cleaner version than one I posted in this forum thread http://forums.adobe.com/thread/518910

Detailed explanation

This example show the selection of a country, so as the user types into the 'Drop-Down List' (actually a textbox) the matching countries are displayed.


This is the code in the change event that updates the items in the list with countries that match the characters typed.

// For each character entered by the user, start by clearing all the current items.Results.CountryList.clearItems();
// If they have cleared the contents of the Search box close the 'Drop-Down List'

if (xfa.event.newText == "")
{
    Results.presence = "hidden";
    Results.CountryList.presence = "hidden";
}
else
{
    var list;
    // Select values to load depending on the matching option selected. We use FormCalc predicates to select the values.
    // The data used in this example has the structure of <country id="AU" name="AUSTRALIA" />
    // So if country matched this element then country.name.value would be "AUSTRALIA", country.id.value would "AU" and
      // country.index would index of AUSTRALIA in the Countries list.
   

    if (this.desc.nodes.namedItem("matchFirstCharacters").value)
    {
        list = xfa.resolveNodes('$record.country.[Lower(Left(name,' + (xfa.event.newText.length) + ')) == "' + xfa.event.newText.toLowerCase() + '"]');
    }
    else
    {
        list = xfa.resolveNodes('$record.country.[At(Lower(name),Lower("'+xfa.event. newText+'")) > 0]');
    }
    // Load the values up    for (var i = 0; i < list.length; i++)
    {
        var country = list.item(i);

        Results.CountryList.addItem(country.name.value, country.index.toString());
    }
    // Make the 'Drop-Down List' visible
    Results.presence = "visible";
    Results.CountryList.presence = "visible";
}

The code has an option to match against the first characters in the list item or any characters in the list item, this is determined by the "matchFirstCharacters" value under the desc element of the Search textbox.  This acts as a custom property of the control and in this sample you can see how it is updated in the change event of the Radio Button List.

This method works best when the 'Drop-Down List' is contained within a positioned subform so that when the list opens it will overflow any content following it, otherwise in a flowed subform the content will be moved down to make room for the list to open.

I did try using a standard Drop-Down List control to acheive this behaviour but was not able to update the items while the Drop-Down List was open.

Download sample Countries.pdf or download the Countries.xdp and Countries.xml.

Adding Ghost Text to LiveCycle Designer form

Problem

Ghost Text is a common way of adding instruction text to a web form, especially if space is tight, but Designer has no built in support.

Solution

Using a null category picture clause and some JavaScript we can achieve a similar effect.

Detailed explanation

Below is an example of ghost text in a form, if the field is empty and has not been selected then we want some information text displayed (in a lighter color and in italics). If the field is selected then we want the information text cleared so the user can enter their response.



In the ready:form event for this field there is the following statements which just set the initial state of the field; (Note: I originally had this code in the initialize event, but as Joe McGloin pointed out in his comments below this does not fire when a Reset button is clicked).

this.execEvent("exit");
this.format.picture.value = "null{'" + this.assist.toolTip.value + "'}";


The trick to this whole approach is setting the format.picture.value to "null{ some text }" which causes Reader to display the some text whenever the field is null/empty. In this example I am displaying the text from the toolTip for the field but this could be any text.

In the enter event I change the font to black and normal posture so it looks like a regular text field once the user starts typing into it.

this.fontColor = "0,0,0";
this.font.posture = "normal";
   
In the enter event I change the font to black and normal posture so it looks like a regular text field once the user starts typing into it. 

In the exit event I change font back to grey, italic if the field is null so it looks like help text. This is also called from the initialize event to set the fields state initially.

if (this.isNull)
{
      this.fontColor = "153,153,153";
      this.font.posture = "italic";
}


There is some more code in the prePrint and postPrint events so the instruction text doesn't get printed out.

this.format.picture.value = "";

And then after printing setting it back;

this.format.picture.value = "null{'" + this.assist.toolTip.value + "'}";

Note: this method will only work on dynamic forms.

I have also added an example to the sample form of using proto elements to define the scripts required for this once and have the fields just reference the script. This requires editing the form in the XML Source view but does save duplicating the script. A proto field can be created by dragging a field from the Hierarchy view to the "Referenced Object" node, make a copy of the field first as this drag is a move not a copy. Once under the "Referenced Object" node it can be renamed and references to it (or parts of it) can be made from the "use" attribute which is common to all elements in the template. So now we can go into the XML Source view and add the following lines to refer to the required scripts.

<event use="protoGhostText.event__enter"/>
<
event use="protoGhostText.event__exit"
/>
<
event use="protoGhostText.event__prePrint"
/>
<
event use="protoGhostText.event__postPrint"
/>
<
event use="protoGhostText.event__form_ready"/>


Download sample, GhostText.pdf.


Saturday 4 May 2013

Numbering rows in a LiveCycle Designer table or repeating subform

Problem

In a dynamic form rows can be added and deleted from a table, but if there is a calculated sequence number then all rows may need to be recalculated depending on where the row was inserted or deleted.

Solution

Using the XFA form dependency tracking for calculate event scripts we can easily recalculate the sequence number with two lines of JavaScript.

Detailed explanation

In the calculate event of cell in the table we can add the following code;

var count = _detail.count;
detail.index + 1;

The purpose of the first line "var count = _detail.count;" is to add _detail.count to the dependency list for this calculation event.  We don't use this value in the script but this means that whenever a row is added or removed all the calculate events on all the rows will fire, generating the correct sequence number if a row is deleted in the middle.

Using _detail, with the underscore,  is a shortcut form of referencing the detail.instanceManager, I could have written this line of code as "var count = detail.instanceManager.count;" and in this case it doesn't really matter but generally I use the underscore syntax as it works even when there are no rows.

LiveCycle Designer Forms support XSLT 1.0 and in this sample is an example of using XSLT to generate an alphabetic sequence and a roman number sequence.



Download sample form AutoNumber.pdf