Search This Blog

Sunday, 9 June 2013

Parsing a currency value in a LiveCycle Designer form

If we have a string value like "$1,234.56" that we want to convert into a JavaScript number value we have a several options
.


We could clean the string up, removing the "$" and "," characters and using the parseFloat function.
 


We could call the FormCalc Parse function and an example of calling FormCalc from JavaScript is on John Brinkman's blog.
 


Or we could assign the string to the formattedValue of a hidden DecimalField and then use the rawValue.  This has the same advantages of the FormCalc parse function, with all it's locale processing, without the overhead of calling FormCalc from JavaScript.
 


All we have to do is set the display pattern of the hidden decimal field to suit, in the example that is;

    num{$zzz,zzz,zz9.88}|num{$zzzzzzzz9.88}

So our parseCurrency code now looks like;

function parseCurrency(string)

{
    var result = 0;

    if (string !== null && string !== undefined)

    {
        HiddenDecimalField.rawValue = null;

        HiddenDecimalField.formattedValue = string;

        if (!HiddenDecimalField.isNull)

        {

            result = HiddenDecimalField.rawValue;
        }
    }
    return result;

}


We have set the rawValue to null before assigning the formattedValue because the rawValue will remain unchanged if the value is not valid.
 


One got cha with using DecimalFields is the JavaScript type returned by the rawValue is normally a number but if you clear the "Limit Trailing Digits" checkbox you get a string rawValue,

In this case change;
 


result = HiddenDecimalField.rawValue;


to



result = parseFloat(HiddenDecimalField.rawValue);
 


You can also use this approach to parse a date with a hidden DateTime field.

Download sample form parseCurrency.

Displaying the Acrobat progress bar in a LiveCycle Designer (XFA) Form

NOTE: As of Reader version 21.001.20135 (Feb 2021) it seems you must begin the Thermometer before you can set the value.

 

When Reader has a lengthy operation to complete it displays a progress bar in the bottom right hand corner, for example;


This is available to a Designer form via the Thermometer object, for example;
 


function showProgress(t)

{
    for (var ii = 0; ii < t.duration; ii++)
    {
        for (var i = 0; i < 500000; i++) {}

        t.value += 1;
        t.text = "Step " + ii;
        if (t.cancelled)
        {
            return false;
        }
    }
    return true;

}


var t = app.thermometer;

t.duration = 10;


t.begin();

t.value = 0;

t.text = "";

var completed = showProgress(t);

if (completed)

{
    app.alert("completed");

}

else

{
    app.alert("cancelled");

}


Download the sample form, Thermometer.pdf.



Saturday, 1 June 2013

Using the Doc.scroll method in a LiveCycle Designer Form

It is common to use the xfa.host.setFocus method to position the user on a particular field in the form. Say they have just clicked the submit button and you have detected that they have left a mandatory field empty.  You give them an error message and use xfa.host.setFocus to position them on the field.
 

The problem with this approach is that setFocus always tries to position the field in the middle of the screen.  This may then mean that the question at the top of the screen, were the eye is naturally drawn to, may not be related the message we have just shown the user.
 


We may also want to move to a non-interactive object on the form, maybe a text object that forms a section heading, or some help text.
 


To achieve this we can use the Doc.scroll method, but this means we need to calculate the y coordinate of the object.  The code I have used to perform the calculation comes from John Brinkman's blog, Layout Methods to Find Page Positions.  All we then need to do is rotate the coordinate (so the point 0,0 is at the bottom left corner of the page)
.


This method is written in JavaScript but is dependent on the FormCalc function UnitValue. To use this code in your forms you need to copy the first subform in the sample called FormCalc (see John's blog Calling FormCalc Functions from JavaScript for more details in this technique) and the script object XFAUtil. 
  


The scrollTo function definition is;


/** Scrolls the specified control to the top of the current view and optional sets focus the the specified control.
 
 * @param {form object} target   (required) -- the control to place at the top of the current view.
 
 * @param {form object} setFocus (optional) -- the control that receives focus.
 
 */

function scrollTo(target, setFocus)
 


To see this in action download the ScrollTo.pdf sample and select a form object from the drop down and click the "ScrollTo" button or "Set Focus" to compare the two methods.



I'm not sure how widely known it is, but the Adobe Reader keyboard short-cut for returning to the previous position in the form is Alt+Left Arrow (or Command + Left Arrow on a Mac).


Reading image properties in an Adobe LiveCycle Designer Form

A LiveCycle Designer form allows a user to insert a picture using an ImageField.  However, there is nothing built-in to allow access the properties of an image.  This set of script fragments makes the Exif, GPS and XMP properties of a JPEG image available.

You can also use these code fragments to determine if the image is a JPEG, GIF, PNG or TIFF.

If you just want to check the size of an image then you can use the rawValue property but as the value is base 64 encoded you will need to allow for an overhead of 33%, so ImageField1.rawValue.length * 3 / 4 will give a rough image size in bytes.

LiveCycle Designer forms allow for JPG, GIF, TIF, and PNG formats.  An ImageField allows the user to select one of these images but only if the file extension matches one of those four.  If the file extension is JPEG, JPE, or TIFF then the image will not show up in the "Select Image File" dialog.  If this is a problem for your users then you can use the importDataObject, something like;

if (event.target.importDataObject("name")) // user did not cancel the "select a data file to import" dialog
{
 var attachmentObject = event.target.getDataObject("name");
 var filetype = attachmentObject.path.substring(attachmentObject.path.lastIndexOf(".") + 1);
 if (filetype === "jpg")
 {
  var imageStream = event.target.getDataObjectContents("name");
  var imageStreamEncoded = Net.streamEncode(imageStream, "base64");
  ImageField1.rawValue = util.stringFromStream(imageStreamEncoded);
 }
}


Back to this sample, to find the date a picture was taken we can now use;

var stream = ByteStream.newByteStream(Base64.decode(ImageField1.rawValue));
var image = JPEG.newJPEG(stream);
if (image.isJPEG)
{
 app.alert(image.getDateTime());
}
else
{
 image = GIF.newGIF(stream);
 if (image.isGIF)
 {
 }
 else
 {
  image = TIFF.newTIFF(stream);
  if (image.isTIFF)
  {
  }
  else
  {
   image = PNG.newPNG(stream);
   if (image.isPNG)
   {
   }
  }
 }
}


There is a bug in the base 64 decode routines built into Adobe Reader but this has not yet proved to be a problem as it only seems to occur towards the end of the file were typically the image data is stored.  However, included in this sample is a JavaScript based base 64 decoded.  This does execute slower so I would only use it if this proves to be a problem, just change the first line to;

var stream = ByteStream.newByteStream(Base64.decode(this.rawValue));

In the sample form there are four options;
  • Turn on JavaScript decoding
  • Display a message if a decode error occurs
  • Stop decoding when the SOF jpeg tag is found.  The only thing that may be missed is the JPEG comment tag, but this seems to be rarely used or superseded by the Exif properties.  This option should speed up the decode.
  • Turn on and off displaying of the decode time.
The code in the sample form starts in the change event of the image field in the top right hand corner (with the "Click here to add photo" caption).  This loads the image and then executes the initialize event of the main ImageField to display all the image properties.  I did it this way because I am using the dataWindow object to allowing scrolling though pictures, without the dataWindow you could perform all processing in the ImageField's change event.



 The form template (without pictures of my cat) can also be downloaded, ImageViewer.xdp

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.