Search This Blog

Friday, 6 December 2013

Formatting telephone numbers

The phone numbers I have to deal with are always 10 digits, but they can be either a two digit prefix, followed by two sets of four digits or a four digit prefix (starting '04') followed by two sets of three digits. For example (02) 1234 1234 or 0400 123 123.



We don't want our users to have to type the spaces and parenthesis but we do want the phone number field to display them once entered,  ignoring them when typed.  This is when we use the display patterns.


 
Because we also want our users to paste a formatted value into the field, including the spaces and parenthesis, we will start with a Text Field.  If we used a Numeric Field then we won't be able to perform a paste,  if the value contains anything but valid numeric characters then the whole paste would fail.



This means we must add some character filtering code to the change event so only digits are accepted.  Filtering characters means testing xfa.event.change when the characters are coming one at a time, as they do when we are typing or, typically, testing xfa.event.newText when we are pasting a value.



But in this case we also want to set the maximum character limit to 10, this means the maximum length of xfa.event.newText is also set to 10 characters.  This becomes a problem when the pasted value is formatted, in which case it will be either 12 or 14 characters. This is when we must use the xfa.event.fullText, which gives the full value before the truncation has occurred.



So our change event code now becomes;


xfa.event.change=(xfa.event.change.length > 1) ?
                   xfa.event.fullText.replace(/\D/g,""):xfa.event.change.replace(/\D/,"");

That is if the value causing the change is longer than one character, remove all non-digit characters, otherwise if the change is one character and that character is not a digit then ignore it.



The final part of our phone number field is to change the display pattern depending on which type of phone number has been entered, so 
our exit event code becomes;


var formatString = "";
if (!this.isNull)

{
    if (LandLinePhoneNumberRegex.test(this.rawValue))
 
    {
        formatString = "'('99')' 9999 9999";
    }
 
  else
 
    {
        if (MobilePhoneRegex.test(this.rawValue))
        {
            formatString = "9999 999 999";
        }
    }

}

this.format.picture.value = formatString;


Setting a display pattern now means we can test for an invalid phone number using;


TelephoneMobile.rawValue === TelephoneMobile.formattedValue


 
This works because the field will only be formatted when a value matches the display pattern, all other values are display as is and so the formatted value will equal the raw value.

Format strings with single quotes

If you ever want a single quote as part of the display pattern, as in a feet and inches measurement like 6'2" then you will need to use two single quotes and wrap then in single quotes.  So four altogether,

text{9''''9"}

 

Sample Form

Formatting telephone numbers.pdf

Tuesday, 19 November 2013

XML Schema minOccurs="0" handling

If you define a data connection using an XML Schema you can specify a minOccur="0" attribute to allow an element to be omitted from the resulting XML.  So an XML Schema element defined as;

<xs:element name="preferredContactMethod" type="preferredContactMethodType" minOccurs="0"/>

Will produce a data connection description of;

<preferredContactMethod dd:minOccur="0" dd:nullType="exclude"/>

And the resulting XML will not contain a preferredContactMethod element if the value is null.

It is the nullType="exclude" attribute that controls the handling of the empty element.  However, Designer only sets this attribute on simple types.  If the minOccurs="0" is on an element that contains child elements or an attribute (that is complex types) then the nullType="exclude" attribute is not added to the data connection description, which can cause the resulting XML to fail schema validation.

This macro will go though the form data description and add the missing nullType="exclude".

Here is a sample form that shows the differences, ComplexTypeNullTypeExclude.pdf.  The same fields are duplicated with the first set having the default behaviour and the second set that has been updated by this macro.

And here is the macro, AddNullTypeExcludeToComplexTypes.zip.  Extract the files to the macros directory of your Designer ES3 (or later) install and you will now get a "Add dd:nullType="exclude" to complex types" option in the Tools ... Macros menu.  (In Designer ES2 you need to extract the files to the Scripts directory of your install and the menu option will be AddNullTypeExcludeToComplexTypes.

Monday, 28 October 2013

Using app.execDialog() in an Adobe Designer Form

Adobe Designer forms can use the app.execDialog() method to show a custom dialog.  The dialog is defined by a JavaScript object literal and this sample is an Adobe Designer form that allows you to add controls, preview your dialog and generate the required object literal.  This is far from a what-you-see-is-what-you-get interface but it does allow you to build up a hierarchy of controls and achieve some complicated dialogs.

To start lets have a look at a very simple dialog, this dialog


Is produced by the following

  description:
  {

   name: "Errors in form",
   elements: [
    {
     type: "view",
     align_children: "align_row",
     elements: [
      {
       type: "static_text",
       name: "Please enter your name",
      },
      {
       type: "ok",
      }
     ]
    }
   ]
  }


Unfortunately for security reason when used within a Designer form you can't specify the dialog title and you also get a "Warning JavaScript Window".  From the console or a Folder Level script you get the dialog without the warnings.



These dialogs can also be used for input (note the addition of a validate function), such as

  description:
  {
   elements: [
    {
     type: "view",
     elements: [
      {
       type: "static_text",
       name: "Please enter your name",
      },
      {
       width: 200,
       height: 22,
       type: "edit_text",
       item_id: "NAME",
      },
      {
       type: "static_text",
       item_id: "ERR1",
       name: "You must enter your name",
      }
     ]
    },
    {
     type: "ok_cancel",
    }
   ]
  },
  validate : function(dialog)
  {
   var isValid = true;
   var elements = dialog.store();
   if (elements["NAME"] === "")
   {
    dialog.visible({ERR1:true});
    isValid = false;
   }
   else
   {
    dialog.visible({ERR1:false});
   }
   return isValid;
  }


Which give this dialog, when the name has been left blank;


This sample, DialogSample.pdf, contains these two dialogs and also one that show most of the  controls available and the properties that go with them;



These dialogs have a hierarchy of controls, that is controls within container controls.  This quickly becomes a very big form.  A form to design a two level dialog contains 3,384 fields, which is enough to get the cooling fans on my computer spinning.  So I am posting two forms one that supports two levels of controls and one that supports one level (which is still 1,452 fields).

Dialog.pdf (3893k)
Dialog.L1.pdf (1936k)

The JavaScript code generated is written to an attachment of the form. There is a stub generated for the event handler of each field and a stub for the validate method.  Be careful writing code in the validate method as if there is no path that returns true then the only way out of the dialog is using the task manager.

These two samples support a number of controls and properties that are not listed in the manual, but are very useful (in these cases I have made note in the tooltips).  Some of the things not documented are;
    • link_text: a hyper link control
    • mclv: a multi-column list view (or grid)
    • slider: a slider but I have not got this to return a value.
    • ok_help, ok_cancel_help, ok_other_help, ok_other_cancel_help controls
    • separator: draw a line horizontal or vertical with optional caption
    • Heading and Title fonts about 10pt and 12pt respectively
    • margin_width, margin_height properties for the view control
    • back_color, gradient_direction, gradient_type for the view control
    • A Dialog.setForeColorRed() method
    • A Dialog.visible() method to show/hide controls
In this sample controls that support a click event also support some actions, such as enabling/disabling other controls, showing/hiding other controls and setting focus on other controls.  Hopefully there will be enough instructions in the form to show you what I mean.

Images

This form started life to generate the hex encoded string.  This is a representation of a 4 channel (ARGB) 8-bits per channel image, that is 8 JavaScript characters per dot.  So images can start to take up a lot of space.  You can get the hex encoded string using the importIcon, getIcon and iconStreamFromIcon methods.  This sample imposes a size limit an image to about 50,000 dots, this is because Designer has trouble with very long lines and this sample uses an XSLT stylesheet to generate the JavaScript literal and the XSLT processor has a limit of 100 levels of recursion. 

Fragments

Each dialog control is defined by a fragment and each fragment is used in three levels, and have a smaller width at each level.  To override the properties of a fragment you need to use the XML Source view and type in the properties you want to override.  For example if the Image fragment had a Border subform and the Border subform had a Propertoes subform then I could override the width values by adding the highlighted XML.

<subform usehref=".\Fragments\ImageControl.xdp#som($template.#subform.Image)" w="162.299mm">
    <subform name="Border" w="169.298mm">
        <subform name="Properties" w="147.299mm"/>
    </subform>
</subform>

One down side of this approach is you will now get the warning message "This fragment reference has local overrides to one or more properties of the source fragment".

ToolTips

As there a many fields that use the same tooltip I have created a separate dataset and used the setProperty element to assign them to the correct field.  This is done in the XML Source view by adding XML under the xfa:dataset element and referencing them in each form field like "
<setProperty target="assist.toolTip" ref="!toolTips.value.[name==&quot;dialogName&quot;]"/>
", which you also need to add using the XML Source view.

Source

The source template, including all the fragments is in the zip file.  Dialog.zip

Friday, 27 September 2013

Adobe LiveCycle Designer Tip #2 - Finding your code

With Designer ES3 came a couple of new options on the hierarchy palette menu, one to show an icon against each form element that contains code and another to show an icon if the form element is bound to a data connection.

The script indicator looks like a scroll and the binding indicator is the one we are used to in the Data View tab.

Friday, 20 September 2013

Adding bookmarks to a Form

Since Designer ES3 came out we have been able to add PDF bookmarks to our forms.  This adds a useful navigation feature and hopefully makes the form more accessible. 

Although Designer would create bookmarks there was no support for them within the Designer UI, you had to go into the XML Source view and enter elements directly, so something like;

<extras name="bookmark">
  <text name="name">Bookmark</text>
  <text name="color">0,0,0</text>
  <text name="style">normal</text>
  <text name="action">gotoPage</text>
<extras>

Radzmar has since written a great macro to generate these elements, http://thelivecycle.blogspot.de/2011/09/xfa-bookmarks.html.

As described in the Adobe Designer help the values for action are;

gotoPage: This is the default value. Focus is shifted to the page where the parent subform starts.
setFocus: Can be used when the parent container is a field. Sets the parent field in focus.
runScript: Triggers JavaScripts to be run.


The gotoPage action seems to go to the top of the page that contains the form control with the bookmark.  This can be confusing for a user if the control is not at the top of the page.

The setFocus action on a subform seems to behave the same as gotoPage and on a field that can receive focus will set focus to that field, but it will also position the field in the middle of the display area, which I didn't like either.

This left me with the runScript action, which I thought I could use my Using Doc.scroll Method In Livecycle Designer method to navigate the form more accurately.  The first thing I found was that the bookmark script runs in the context of the Doc object.  This meant to reference my script object and form controls I had to prefix them with xfa.form.form1, where form1 is the top most subform of my form.  The next thing I found was that event.target did not return the Doc object as it does in a LiveCycle event.  This was not a great problem as the Doc object was my context so I could pass it to my scrollTo method.  Now my bookmark elements look like;

<extras name="bookmark">
  <text name="name">Bookmark</text>
  <text name="color">0,0,0</text>
  <text name="style">runScript</text>
  <text name="action">xfa.form.form1.XFAUtil.scrollTo(xfa.form.form1.Body.PartC, undefined, this);</text>   
<extras>

 
The scrollTo method signature has now changed to;  

/** 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.
 *  @param {Doc object}  doc      (optional)    -- the Doc object (required when used in a bookmark).
 */
function scrollTo(target, setFocus, doc)
  

Samples

Here are three samples;
Remember that gotoPage and setFocus navigate to the top of the page so there may be no movement at all if you click between two bookmarks that are close together.

Testing

When testing bookmarks in LiveCycle Designer you need to be able to get to the navigation panels that are normally display on the right side of Adobe Reader.  To view these in preview mode press the ESC key.  This will then show the Acrobat toolbar and the navigation panels,  but not there doesn't seem to be the Tools menu available. 

You can also press Crtl + Shift + F5 to open the navigation panels and set focus to them directly.





Monday, 8 July 2013

Adobe LiveCycle Designer Tip #1

One line of code I add to every form is;

event.target.viewState = { pageViewLayoutMode:2, pageViewZoom:0.964 };

This bit of JavaScript I put in the docReady event of the top subform.

The first part, pageViewLayoutMode:2, has the same effect as selecting "Enable Scrolling" from the View ... Page Display menu.  This allows the form to scroll smoothly, especially noticeable when there are large fields, like multiline text boxes or a list box close to the top or bottom of a page.

The second part, pageViewZoom:0.964, has become more important as more people have wide screens.  The default zoom setting for Adobe Reader is fit to width, which with a wide screen can give a letterbox slot view of your form.  If your form has only one page then you could set the zoom level to 100%, but if the form is longer then the vertical scroll bar down the side will take up enough space for a horizontal scroll bar to appear. 

So setting the zoom level to 96.4% is as close to 100% you can go without the horizontal scroll bar appearing.

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.


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.