# Widgets.pm - The Widgets Class that provides HTML related widgets.
# Created by James Pattie, 2005-03-08.

# Copyright (c) 2005 Xperience, Inc. http://www.pcxperience.com/
# All rights reserved.  This program is free software; you can redistribute it
# and/or modify it under the same terms as Perl itself.

package HTMLObject::Widgets;
use strict;
use HTMLObject::ErrorBase;
use HTMLObject::Base;
use vars qw($AUTOLOAD $VERSION @ISA @EXPORT @EXPORT_OK);

require Exporter;

@ISA = qw(HTMLObject::ErrorBase Exporter AutoLoader);
@EXPORT = qw(
);

$VERSION = '2.30';

=head1 NAME

HTMLObject::Widgets - HTML Widgets for HTMLObject users.

=head1 SYNOPSIS

  use HTMLObject::Widgets;
  my $widgets = HTMLObject::Widgets->new();

  $doc->print($widgets->generatePickerCode());  # fill in the parameters to the picker code.

=head1 DESCRIPTION

HTMLObject::Widgets provides a centralized module for all the extra
methods that do non-standard html.

For example, the Date and Color pickers.

=head1 Exported FUNCTIONS

=over 4

=item ref new()

  instantiates the object.

=cut
sub new
{
  my $class = shift;
  my $self = $class->SUPER::new(@_);

  if (!$self->HTMLObject::Widgets::isValid)
  {
    return $self;
  }

  $self->{baseObj} = HTMLObject::Base->new();

  return $self;
}

=item bool isValid()

 returns 1 if everything is ok, 0 otherwise.

=cut
sub isValid
{
  my $self = shift;

  # make sure our Parent class is valid.
  if (!$self->SUPER::isValid())
  {
    $self->prefixError();
    return 0;
  }

  # validate our parameters.

  if ($self->numInvalid() > 0 || $self->numMissing() > 0)
  {
    $self->error($self->genErrorString("all"));
    return 0;
  }
  return 1;
}

=item hash generatePickerCode(type, baseUrl, phrase, width, height, form, itemName, itemValue, windowName, onClick, onChange, link, class, linkClass, year, seperator, displayPrevNextDateLinks, style)

 This is called from HTMLObject::Form->generate() for -Type = date-picker, datePicker, colorPicker or color-picker.

 Returns the HTML fragment needed to display the phrase as a link
 that allows the user to select a color or date and display their
 selection in the edit field to the right of the link and the
 javascript code to Include the necessary javascript library.

 If an error occured, we return undef and set the error string, so use
 $obj->error() to determine if an error happened and then
 $obj->errorMessage to get the message to display.

 The returned hash has the body and javascriptIncludes entries
 defined so you can just pass the hash to the print() method
 or extract the code yourself.

 For developers needing to get the link/Phrase part seperate
 from the input field definition, use the _link_ and _input_
 entries in the returned hash to get this information.

 requires:
  type: 'color', 'date'
  baseUrl: The url of the server, minus the /htmlobject part, that
    the /htmlobject directory exists at.  Can be empty to indicate
    the current server.
  form: name of the form we are currently in.
  itemName: name of the text field we are creating.
  windowName: name of the window we are going to create.
  phrase:  the string to display.

 optional:
  onClick:  specify any JavaScript code you need executed when the
            link is selected.  This is only valid when link = true.
  onChange: specify any JavaScript code you want executed when the
            edit field is modified. This will only be run if the
            validation code we output returned true to indicate
            you have a valid color or date specified.
  width: defaults to 640
  height: defaults to 410
  itemValue: the value to display in the text field.  defaults to "".
  link: true (1) - display the phrase as a link, false (0) - don't
    make it a link.
  class: specify the CSS class you want applied to the phrase and
    input text box.
  style: specify the CSS style you want applied to the phrase and
    input text box.
  linkClass: specify the CSS class the link is part of.
  year: only valid when type = 'date'.  Specifies the year to default
        to if the user doesn't specify the year when entering the
        date.
  seperator: only valid when type = 'date'.  Specifies the date
             seperator to work with.  Defaults to '-'.
  displayPrevNextDateLinks: true (1) - display the Prev/Next links,
    false (0) - don't display the Prev/Next links.  Defaults to
    true (1).

  Notes: when displaying a date picker, 2 extra links are now
  output around the text box, if displayPrevNextDateLinks is true.
  They are < and >, to allow you to quickly change the current date 1
  day backwards or forwards.  The output will look like:
  Date < [text box] >, if phrase = "Date".

=cut

sub generatePickerCode
{
  my $self = shift;
  my %args = ( type => "color", width => "640", height => "410", form => "",
               itemName => "", itemValue => "", windowName => "", phrase => "", onChange => "",
               link => 1, year => "", seperator => "-", class => "", linkClass => "", onClick => "",
               baseUrl => "", displayPrevNextDateLinks => 1, style => "", @_ );
  my $type = $args{type};
  my $width = $args{width};
  my $height = $args{height};
  my $form = $args{form};
  my $itemName = $args{itemName};
  my $itemValue = $args{itemValue};
  my $windowName = $args{windowName};
  my $phrase = $args{phrase};
  my $onChange = $args{onChange};
  my $link = $args{link};
  my $year = $args{year};
  my $seperator = $args{seperator};
  my $class = $args{class};
  my $style = $args{style};
  my $linkClass = $args{linkClass};
  my $onClick = $args{onClick};
  my $baseUrl = $args{baseUrl};
  my $displayPrevNextDateLinks = $args{displayPrevNextDateLinks};
  my %result = ();
  my $result = "";

  # fixup the link value
  $link = ($link eq "true" ? 1 : ($link eq "false" ? 0 : $link));

  my $errorStr = "";

  if ($type !~ /^(color|date)$/)
  {
    $self->invalid("type", $type);
  }
  if ($width !~ /^(\d+)$/)
  {
    $self->invalid("width", $width);
  }
  if ($height !~ /^(\d+)$/)
  {
    $self->invalid("height", $height);
  }
  if ($form eq "")
  {
    $self->missing("form");
  }
  if ($itemName eq "")
  {
    $self->missing("itemName");
  }
  if ($windowName eq "")
  {
    $windowName = $type;  # set it equal to the type (date, color)
  }
  if ($phrase eq "")
  {
    $self->missing("phrase");
  }
  if ($link !~ /^(1|0)$/)
  {
    $self->invalid("link", $link, "Can only be 1 or 0.");
  }
  if ($type =~ /^(date)$/)
  {
    if ($year !~ /^(\d{4})$/)
    {
      $self->invalid("year", $year);
    }
    if ($seperator !~ /^(-|\\|\/)$/)
    {
      $self->invalid("seperator", $seperator);
    }
    $displayPrevNextDateLinks = ($displayPrevNextDateLinks eq "true" ? 1 : ($displayPrevNextDateLinks eq "false" ? 0 : $displayPrevNextDateLinks));
  }

  if ($self->numInvalid() > 0 || $self->numMissing() > 0)
  {
    $self->error($self->genErrorString("all"));
    return undef;
  }

  # formEncode the phrase to protect from xss.
  $phrase = $self->baseObj->formEncode(string => $phrase, sequence => "formatting");

  # build up the form entry.
  my $jsUrl = $baseUrl . "/htmlobject/";

  my $onClickCode = "window." . ($type eq "color" ? "colorField" : "dateField") . "=document.$form.$itemName; " . $type . "Win=window.open('$jsUrl" . ($type eq "color" ? "color_picker.html" : "calendar.html") . "', '$windowName', 'width=$width,height=$height,resizable,scrollbars,status'); " . ($onClick ? $onClick . ($onClick !~ /;$/ ? ";" : "") . " " : "") . "return false;";

  my $linkStr = ($link ? "<a href=\"#\" " . ($linkClass ? "class=\"$linkClass\" " : "") . "onclick=\"$onClickCode\" onmouseover=\"window.status='PopUp " . ($type eq "color" ? "Color" : "Calendar") . " Picker'; return true;\">" : "") . ($class || $style ? "<span" . ($class ? " class=\"$class\"" : "") . ($style ? " style=\"$style\"" : "") . ">" : "") . $phrase . ($class || $style ? "</span>" : "") . ($link ? "</a>" : "");

  my $inputStr = ($type eq "date" && $displayPrevNextDateLinks ? qq{<a href="#" } . ($linkClass ? "class=\"$linkClass\" " : "") . qq{ onclick="calcDatePrev(document.$form.$itemName); return false;" onmouseover="window.status='Previous Day'; return true;">&lt;</a> } : "") . "<input type=\"text\" name=\"$itemName\" value=\"$itemValue\"" . (exists $args{disabled} ? " disabled=\"$args{disabled}\"" : "") . ($class ? "class=\"$class\"" : "") . ($style ? "style=\"$style\"" : "") . " onchange=\"javascript: if (" . ($type eq "color" ? "isValidColor(this)" : "fixupISODate(this, '$seperator', '$year')") . ") { " . ($onChange ? "$onChange" : "") . " }\"" . ($type eq "date" ? " size=\"10\" maxlength=\"10\"" : "") . ">" . ($type eq "date" && $displayPrevNextDateLinks ? qq{ <a href="#" } . ($linkClass ? "class=\"$linkClass\" " : "") . qq{ onclick="calcDateNext(document.$form.$itemName); return false;" onmouseover="window.status='Next Day'; return true;">&gt;</a>} : "");

  $result{body} = $linkStr . " " . $inputStr;
  $result{_link_} = $linkStr;
  $result{_input_} = $inputStr;
  $result{javascriptIncludes} = "<script language=\"JavaScript1.5\" src=\"$jsUrl" . "js/form_methods.js\" type=\"text/javascript\"></script>";

  return %result;
}

=item % generatePopulator(name, rows, required, options, manual, optionsTypes, customLabel, htmlTemplate,
selectLocation, clearPhrase, tableClass, tableStyle, selectClass, selectStyle, debugLevel, numSelectRows,
displayColumnHeaders, columnHeaders, displayLabels, -ReadOnly, -ReadOnlyMode, -ReadOnlyArgs, -ReadOnlyDisplayType)

 This is called from HTMLObject::Form->generate() for -Type = populator.

 required:
   name - name of the "populator" widget.  This is the prefix
     value that all generated form items will start with.
   rows - how many rows need to be generated.  Defaults to 1.
   required - how many rows are required to be filled in by the user.
     Defaults to 1.
   options - -Options array as provided to the
     HTMLObject::Form->generate() method.  Used to determine how many
     form items per row to generate.

     Each entry is a hash with the following values:
       label - value to display in the select box for this entry.
       data - array ref of column entries.

     All entries must have the same number of elements in their
     data array.
   manual - (boolean) if true (1), then we just generate the necessary
     javascript since the caller MUST have populated the template,
     profile and data hashes.  if false (0), then we generate the html
     template snippet, profile and data structures and the necessary
     javascript.

 optional:
   optionsTypes - array of hashes that defines what each columns form
     field is to be.  If specified as a string equal to:
       text - all form items are text fields
       datePicker - all form items are date pickers
       colorPicker - all form items are color pickers
     If not specified, it defaults to "text".
     If you provide the array of hashes, the hashes must be valid
     data structures that the HTMLObject::Form->generate() would
     accept as part of its data hash.  You do not need to define the
     name of the form item (as in the data hash you would pass to
     generate()), just the attributes for the form item, like
     -Type, -Label, -Value, etc.
     If you specify -Type => "searchBox", do not specify the hidden
     field support or use an associative array, as the code does not
     currently support this.
   customLabel - if optionsTypes is one of our special cases:
     text, datePicker, colorPicker
     then you can specify customLabel to provide the label for each
     form item generated, since they are all going to be the same.
     Defaults to "".
   htmlTemplate - user defined html snippet that represents the users
     layout for a row.  If defined and manual = 0, we use it for each
     generated row substituting the field_x_y (where y >= 0 and < the
     number of columns to be generated) values with the form names
     being generated.  The template should only define <td> cells
     and not the parent <tr> cell.
     Ex:
     htmlTemplate = "<td>#LRFI=field_x_0#</td><td>#LRFI=field_x_1#</td>"
     where we will substitute field_x_ with the field name and the
     row number we are currently processing when we generate the html.
   selectLocation - left or right.  specifies if the select box should
     be to the left of the rows or to the right.  Defaults to 'right'.
   clearPhrase - phrase to use for the Clear Row buttons.  Defaults to
     'clear'.
   tableClass - class to apply to the outer table.  Defaults to ''.
   tableStyle - style to apply to the outer table.  Defaults to ''.
   selectClass - class to apply to the select box.  Defaults to ''.
   selectStyle - style to apply to the select box.  Defaults to ''.
   debugLevel - indicate how much debug info you want the javascript
     code to generate.  Defaults to 0.
     Levels are:
       0 = none
       1 = show strings to be evaluated
       2 = show results of the evaluted strings
   numSelectRows - number of rows the select box should show.
     Minimum amount allowed is 5. Defaults to 10.
   displayColumnHeaders - boolean.  If 1 (true), then we generate
     a header row displaying the label for each column.  Defaults
     to 1 (true).
   columnHeaders - array of custom labels for each column.  If
     displayColumnHeaders is true and you do not want the label
     from the row to be displayed as the column header, then use
     this to specify each columns header label.
   displayLabels - boolean.  If 1 (true), then we output the
     html template tag to cause the -Label to be generated.
     Defaults to 0 (false), since the displayColumnHeaders is
     turned on by default.  If the entry is a date-picker or
     color-picker, the label will still be displayed so that
     the user can have the popup to select a date or color.
     If customLabel is defined, then it will be used instead.

   The following entries are passed into the generated form items, so
     that the developer can generate a read-only version of the
     populator widget.

   -ReadOnly - boolean.
   -ReadOnlyMode - (DOM or text)
   -ReadOnlyArgs - string
   -ReadOnlyDisplayType - (both, name or value)

 returns:
   hash with the following entries:
     html - generated html hash (contains javascript, body, onsubmit, link).
       body  - html template replacement for the generate() method.
       javascript - the javascript code needed for this widget to work.
       onsubmit - the onsubmit code to be added to the form.
         The form that outputs this widget must take care to add the
         onsubmit string to it's own onsubmit string.  By default,
         the onsubmit handler generated, only returns false if the
         validation method it calls fails.  Otherwise, it falls
         through which will cause the form to submit.
       link - string specifying a <link> tag to pull in an external
         css stylesheet.
     data - generated data hash that will be used by the generate()
       method to actually create the necessary form items
     profile - generated profile hash to specify what form items are
       required and/or how to validate them if there are special
       validation checks needed.
     order - array of form items in the order they should be processed,
       suitable for inclusion in the order array passed to the
       generate() method.

 summary:

 The generatePopulator() method will output a complex form selection
 widget that uses a select box to determine the values to populate the
 next available row.  The rows will be all the same, but there is no
 requirement that the columns have to be all the same.  You can specify
 a very complex input screen by defining the optionsTypes array.

 The generated html will consist of a table that takes up 100% of the
 available space.  It will be split into 2 columns, with the select box
 in the left or right column, based upon the selectLocation parameter.
 The other column will have a child table defined and for each generated
 row of form items, we wrap the htmlTemplate in the <tr></tr> tags.
 If htmlTemplate is not specified, then each form item is defined as:
   <td align="right" valign="top">#RL=field_x_y#</td>
   <td align="left">#FI=field_x_y#</td>

 where field_x_y means:
   field = name
   x = row #, starting at 0
   y = column #, starting at 0

 The generated javascript will store the options in a javascript array
 of objects with values = col0, col1, col2, etc. allowing quick and
 easy lookup and assignment.  The select box, will only have the label
 to be displayed for each selection.  The selectedIndex value will be
 used to index into the javascript array to determine the data to
 populate.  This way we don't have to do string splitting, etc., which
 is expensive and can have issues if the user wants to use our seperator
 value in their data.

 The javascript will attempt to find an empty row and populate it.  If
 no empty rows are found, then it will popup an alert and inform the
 user of this condition.

 On submission, if required > 0, then the javascript will first attempt
 to make sure that at least required rows were populated and then will
 make sure that required rows starting at index 0 and incrementing by
 1 are populated to meet the FormValidator requirements as specified
 by the profile.


 Sample usage for specifying in the HTMLObject::Form data hash:

  data{foo}->{bar} = { -Type => "populator", -Rows => 4, -Required => 1,
    -Options => \%selectOptions, manual => 0,
    -OptionsTypes => "datePicker", -SelectLocation => "left" };

=cut
sub generatePopulator
{
  my $self = shift;
  my %args = ( name => "", rows => 1, required => 1, options => [], manual => 0,
               optionsTypes => "text", htmlTemplate => "", selectLocation => "right",
               clearPhrase => "clear", tableClass => "", tableStyle => "",
               selectClass => "", selectStyle => "", customLabel => "",
               debugLevel => 0, numSelectRows => 10, displayColumnHeaders => 1,
               columnHeaders => undef, displayLabels => 0, @_ );

  my $name = $args{name};
  my $rows = $args{rows};
  my $required = $args{required};
  my $options = $args{options};
  my $manual = $args{manual};
  my $optionsTypes = $args{optionsTypes};
  my $customLabel = $args{customLabel};
  my $htmlTemplate = $args{htmlTemplate};
  my $selectLocation = $args{selectLocation};
  my $clearPhrase = $args{clearPhrase};
  my $tableClass = $args{tableClass};
  my $tableStyle = $args{tableStyle};
  my $selectClass = $args{selectClass};
  my $selectStyle = $args{selectStyle};
  my $debugLevel = $args{debugLevel};
  my $numSelectRows = $args{numSelectRows};
  my $displayColumnHeaders = $args{displayColumnHeaders};
  my $columnHeaders = $args{columnHeaders};
  my $displayLabels = $args{displayLabels};
  my $numColumns = -1;

  my %readOnlyArgs = ( -ReadOnly => $args{-ReadOnly}, -ReadOnlyMode => $args{-ReadOnlyMode},
                       -ReadOnlyArgs => $args{-ReadOnlyArgs}, -ReadOnlyDisplayType => $args{-ReadOnlyDisplayType} );
  my $readOnly = (exists $args{-ReadOnly} && $args{-ReadOnly} ? 1 : 0);
  %readOnlyArgs = () if (!$readOnly);

  my %result = ( html => { javascript => "", body => "" }, data => {}, profile => {}, order => [] );

  # fixup the manual value
  $manual = ($manual eq "true" ? 1 : ($manual eq "false" ? 0 : $manual));

  # do validation of input.
  if ($name eq "")
  {
    $self->missing("name");
  }
  if ($debugLevel !~ /^(0|1|2)$/)
  {
    $self->invalid("debugLevel", $debugLevel, "valid levels are 0, 1, 2");
  }
  if ($numSelectRows !~ /^(\d+)$/)
  {
    $self->invalid("numSelectRows", $numSelectRows, "must be an integer >= 5");
  }
  elsif ($numSelectRows < 5)
  {
    $self->invalid("numSelectRows", $numSelectRows, "must be an integer >= 5");
  }
  if ($rows !~ /^(\d+)$/)
  {
    $self->invalid("rows", $rows, "must be a positive number >= 0");
  }
  if ($required !~ /^(\d+)$/)
  {
    $self->invalid("required", $required, "must be a positive number >= 0");
  }
  if ($displayColumnHeaders !~ /^(0|1)$/)
  {
    $self->invalid("displayColumnHeaders", $displayColumnHeaders, "this is a boolean value");
  }
  if ($displayLabels !~ /^(0|1)$/)
  {
    $self->invalid("displayLabels", $displayLabels, "this is a boolean value");
  }
  if (!$displayLabels && !$displayColumnHeaders)
  {
    $self->invalid("displayLabels & displayColumnHeaders", 0, "You must either displayLabels or displayColumnHeaders.  They can not both be off.");
  }
  if (ref ($options) ne "ARRAY")
  {
    $self->invalid("options", ref($options), "must be an array ref");
  }
  elsif (scalar @{$options} == 0)
  {
    $self->missing("options", "must have at least 1 entry");
  }
  else
  {
    # now walk over the entries and make sure they appear valid.
    for (my $i=0; $i < scalar @{$options}; $i++)
    {
      if (ref ($options->[$i]) ne "HASH")
      {
        $self->invalid("options[$i]", ref($options->[$i]), "must be a hash ref");
      }
      else
      {
        if (! exists $options->[$i]->{label})
        {
          $self->missing("options[$i]->{label}");
        }
        elsif (length $options->[$i]->{label} == 0)
        {
          $self->invalid("options[$i]->{label}", "", "You must specify the label to display");
        }
        if (! exists $options->[$i]->{data})
        {
          $self->missing("options[$i]->{data}");
        }
        elsif (ref ($options->[$i]->{data}) ne "ARRAY")
        {
          $self->invalid("options[$i]->{data}", ref($options->[$i]->{data}), "must be an array ref");
        }
        else
        {
          my $tmpColumns = scalar @{$options->[$i]->{data}};
          $numColumns = $tmpColumns  if ($i == 0);
          if ($tmpColumns == 0 || $tmpColumns != $numColumns)
          {
            $self->invalid("options[$i]->{data}", $tmpColumns, "length can not be = 0 or is not = '$numColumns'");
          }
        }
      }
    }
  }
  if ($displayColumnHeaders == 1)
  {
    if (ref ($columnHeaders) eq "ARRAY" && scalar @{$columnHeaders} != $numColumns)
    {
      $self->invalid("scalar \@columnHeaders", int(scalar @{$columnHeaders}), "You must have $numColumns entries defined!");
    }
  }
  if ($manual !~ /^(0|1)$/)
  {
    $self->invalid("manual", $manual, "must be 0 or 1");
  }
  if ($required > $rows)
  {
    $self->invalid("required", $required, "can not be > than $rows");
  }
  if (defined $optionsTypes)
  {
    if (ref ($optionsTypes) ne "ARRAY")
    {
      if ($optionsTypes !~ /^(text|datePicker|date-picker|colorPicker|color-picker)$/)
      {
        $self->invalid("optionsTypes", $optionsTypes, "valid values: text, datePicker, date-picker, colorPicker, color-picker or array of data hash entries");
      }
    }
    else
    {
      if (scalar @{$optionsTypes} != $numColumns)
      {
        $self->invalid("scalar \@optionsTypes", int(scalar @{$optionsTypes}), "You must have $numColumns entries defined!");
      }
      else
      {
        # now loop over the array and make sure we have hashes that look semi valid.
        for (my $i=0; $i < scalar @{$optionsTypes}; $i++)
        {
          if (ref($optionsTypes->[$i]) ne "HASH")
          {
            $self->invalid("optionsTypes[$i]", ref($optionsTypes->[$i]), "must be an array ref");
          }
          else
          {
            # check for -Type as a minimum.
            if (!exists $optionsTypes->[$i]->{-Type})
            {
              $self->missing("optionsTypes[$i]->{-Type}", "-Type must be defined");
            }
            if ($optionsTypes->[$i]->{-Type} !~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|select|multi-select|searchBox)$/)
            {
              $self->invalid("optionsTypes[$i]->{-Type}", $optionsTypes->[$i]->{-Type}, "can only be: text, password, textarea, date-picker, datePicker, color-picker, colorPicker, calculator, select, multi-select, searchBox");
            }
          }
        }
      }
    }
  }
  if ($selectLocation !~ /^(left|right)$/)
  {
    $self->invalid("selectLocation", $selectLocation, "must be 'left' or 'right'");
  }
  if ($self->numInvalid() > 0 || $self->numMissing() > 0)
  {
    $self->error($self->genErrorString("all"));
    return %result;
  }

  # at this point, we appear to be valid.

  # lets start generating the javascript data structures.
  my $jscript = "\n";
  my $jscriptSelect = $name . "PopulatorSelect";
  my $jscriptClearRow = $name . "PopulatorClearRow";

  if (!$readOnly)
  {
    my $jscriptData = $name . "PopulatorData";
    $jscript .= "// data for populator widget = '$name'\n";
    $jscript .= "var $jscriptData = new Array;\n";
    for (my $i=0; $i < scalar @{$options}; $i++)
    {
      $jscript .= $jscriptData . "[$i] = new Array(";
      for (my $j=0; $j < scalar @{$options->[$i]->{data}}; $j++)
      {
        (my $value = $options->[$i]->{data}->[$j]) =~ s/'/\\'/g;
        $jscript .= ", " if ($j > 0);
        $jscript .= "'$value'";
      }
      $jscript .= ");\n";
    }

    my $jscriptDebugLevel = $name . "PopulatorDebugLevel";
    my $jscriptFindNextRow = $name . "PopulatorFindNextRow";
    my $jscriptCountPopulatedRows = $name . "PopulatorCountPopulatedRows";
    my $jscriptPopulateRow = $name . "PopulatorPopulateRow";
    my $jscriptMoveRow = $name . "PopulatorMoveRow";
    my $jscriptLookupIndex = $name . "PopulatorLookupIndex";
    my $jscriptConsolidateRows = $name . "PopulatorConsolidateRows";
    my $jscriptEnforceRequiredRows = $name . "PopulatorEnforceRequiredRows";
    # now generate the actual populate method for when the user selects an entry from the select box.
    $jscript .= qq(
var $jscriptDebugLevel = $debugLevel;

// finds the next empty or full row and returns it's index.
// returns -1 if we can't find the next type of row requested.
// returns -2 if an error had occured when doing one of the evals.
// if empty = true, then we are looking for an empty row.
// if empty = false, then we are looking for a populated row.
// You can optionally specify the starting index by passing in
// a third argument.
function $jscriptFindNextRow(field, empty)
{
  var startIndex = 0;
  if (arguments.length > 2)
  {
    startIndex = arguments[2];
  }
  // find the next empty/full row.
  for (var i=startIndex; i < $rows; i++)
  {
    var rowEmpty = true;
    for (var j=0; j < $numColumns && rowEmpty; j++)
    {
      var t = "result = field.form.$name" + "_" + i + "_" + j + );

    # figure out what type of field we are working with, and thus what we should be checking for being empty, etc.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{".value"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{".selectedIndex"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{".value"};
    }
    $jscript .= qq{;
      var result = "";
      if ($jscriptDebugLevel > 0)
        alert("$name - $jscriptFindNextRow():\\n" + t.toString());
      try {
        eval(t.toString());
      }
      catch (e)
      {
        alert("$name - $jscriptFindNextRow():\\n" + e);
        return -2;
      }
      var result2 = false;
      var t2 = "if (" + };
    if (ref($optionsTypes) eq "ARRAY")
    {
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{"result.length > 0"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{"result != -1"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
    }
    else
    {
      $jscript .= qq{"result.length > 0"};
    }
    $jscript .= qq{ + ") { result2 = true; } else { result2 = false; }";};
    $jscript .= qq(
      if ($jscriptDebugLevel > 0)
        alert("$name - $jscriptFindNextRow():\\n" + t2.toString());
      try {
        eval(t2.toString());
      }
      catch (e)
      {
        alert("$name - $jscriptFindNextRow():\\n" + e);
        return -2;
      }
      if ($jscriptDebugLevel > 1)
        alert("$name - $jscriptFindNextRow():\\n" + "result2 = '" + result2 + "', empty = '" + empty + "'");
      if (result2)
      {
        rowEmpty = false;
      }
    }
    if (rowEmpty && empty)
    {
      return i;
    }
    else if (!rowEmpty && !empty)
    {
      return i;
    }
  }

  return -1;  // signal we didn't find whatever type of row was requested.
}

function $jscriptLookupIndex(field, value)
{
  var index = -1;

  for (var i=0; i < field.options.length; i++)
  {
    if (field.options[i].value == value)
    {
      return i;
    }
  }

  return index;
}

function $jscriptCountPopulatedRows(field)
{
  var num = 0;

  for (var i=0; i < $rows; i++)
  {
    // increment our count, if the next non-empty row is the row we are on.
    if ($jscriptFindNextRow(field, false, i) == i)
    {
      num++;
    }
  }

  return num;
}

function $jscriptSelect(field)
{
  var index = field.selectedIndex;

  if (index == -1 || index > field.options.length)
  {
    alert("$name - $jscriptSelect():\\n" + "selected index = '" + index + "' is invalid!\\nYou must select an item to populate.");
    return;
  }

  // find an empty row.
  var row = $jscriptFindNextRow(field, true);
  if (row == -1)
  {
    alert("$name - $jscriptSelect():\\n" + "You do not have any empty rows to be populated!\\nPlease empty a row and try again.");
  }
  else
  {
    // do the population from the selectedIndex value.
    $jscriptPopulateRow(field, index, row, 'array');
  }

  // now unselect the selected item so it can be re-selected, if need be.
  field.selectedIndex = -1;
}

function $jscriptClearRow(field, index)
{
  if (index < 0 || index > $rows)
  {
    alert("$name - $jscriptClearRow():\\nindex = '" + index + "' is out of bounds!");
    return;
  }

  for (var j=0; j < $numColumns; j++)
  {
    var t = "field.form.$name" + "_" + index + "_" + j + );

    # figure out what type of field we are working with, and thus what we should be assigning to.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{".value"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{".selectedIndex"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{".value"};
    }
    $jscript .= qq{ + " = " + };
    # figure out what type of field we are working with, and thus what we should be assigning to.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{"''"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{"-1"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{"''"};
    }
    $jscript .= qq( + ";";
    if ($jscriptDebugLevel > 0)
      alert("$name - $jscriptClearRow():\\n" + t.toString());
    try {
      eval(t.toString());
    }
    catch (e)
    {
      alert("$name - $jscriptClearRow():\\n" + e);
      return;
    }
  }
}

// Takes care of doing the form assignment either from the array
// or from another form row.  source = 'array' or 'form' dictates
// this.
// returns 0 on error, 1 on success.
function $jscriptPopulateRow(field, srcRow, destRow, source)
{
  if (source == 'form')
  {
    if (srcRow < 0 || srcRow > $rows)
    {
      alert("$name - $jscriptPopulateRow():\\nsrcRow = '" + srcRow + "' is out of bounds!");
      return 0;
    }
  }
  else if (source == 'array')
  {
    if (srcRow < 0 || srcRow > $jscriptData.length)
    {
      alert("$name - $jscriptPopulateRow():\\nsrcRow = '" + srcRow + "' is out of bounds!");
      return 0;
    }
  }
  else
  {
    alert("$name - $jscriptPopulateRow():\\n" + "source = '" + source + "' is invalid!  Can only be 'array' or 'form'.");
    return 0;
  }

  if (destRow < 0 || destRow > $rows)
  {
    alert("$name - $jscriptPopulateRow():\\ndestRow = '" + destRow + "' is out of bounds!");
    return 0;
  }

  // Do the assignment.
  for (var j=0; j < $numColumns; j++)
  {
    var t = "field.form.$name" + "_" + destRow + "_" + j + );

    # figure out what type of field we are working with, and thus what we should be assigning to.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{".value"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{".selectedIndex"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{".value"};
    }
    $jscript .= qq{ + " = " + (source == 'form' ? "field.form.$name" + "_" + srcRow + "_" + j + };

    # figure out what type of field we are working with, and thus what we should be assigning from.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{".value"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          $jscript .= qq{".selectedIndex"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{".value"};
    }
    $jscript .= qq{ : };

    # figure out what type of field we are working with, and thus what we should be assigning from.
    if (ref($optionsTypes) eq "ARRAY")
    {
      $jscript .= "(";
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(text|password|textarea|date-picker|datePicker|color-picker|colorPicker|calculator|searchBox)$/)
        {
          $jscript .= qq{"$jscriptData" + "[" + srcRow + "][" + j + "]"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(select|multi-select)$/)
        {
          # using destRow for the form field to work with, since srcRow may be out of bounds.
          $jscript .= qq{"$jscriptLookupIndex(field.form.$name" + "_" + destRow + "_" + j + ", $jscriptData" + "[" + srcRow + "][" + j + "])"};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
      $jscript .= ")";
    }
    else
    {
      $jscript .= qq{"$jscriptData" + "[" + srcRow + "][" + j + "]"};
    }
    $jscript .= qq{);};

    $jscript .= qq(
    if ($jscriptDebugLevel > 0)
      alert("$name - $jscriptPopulateRow():\\n" + t.toString());
    try {
      eval(t.toString());
    }
    catch (e)
    {
      alert("$name - $jscriptPopulateRow():\\n" + e + "\\n" + t.toString());
      return 0;
    }

    // Do any data validation checks.
    var d=new Date();
    var t2 = );

    # see if we need to trigger a picker-code validation routine for date/color pickers.
    if (ref($optionsTypes) eq "ARRAY")
    {
      for (my $j=0; $j < scalar @{$optionsTypes}; $j++)
      {
        $jscript .= " : " if ($j > 0);  # output the : false side if > 0
        $jscript .= "(j == $j ? " if ($j < scalar @{$optionsTypes} - 1); # output the conditional for all but the last entry.
        # now do the lookup and output the javascript variable to work with.
        if ($optionsTypes->[$j]->{-Type} =~ /^(color-picker|colorPicker)$/)
        {
          $jscript .= qq{"isValidColor(field.form.$name" + "_" + destRow + "_" + j + ")"};
        }
        elsif ($optionsTypes->[$j]->{-Type} =~ /^(date-picker|datePicker)$/)
        {
          #$jscript .= qq{"fixupISODate(field.form.$name" + "_" + srcRow + "_" + j + ", '-', (var d=new Date(); d.getFullYear();))"};
          $jscript .= qq{"fixupISODate(field.form.$name" + "_" + destRow + "_" + j + ", '-', '" + d.getFullYear() + "')"};
        }
        else
        {
          $jscript .= qq{""};
        }
      }
      $jscript .= ")" x (int(scalar @{$optionsTypes}) - 1);  # generate the closing parantheses.
    }
    else
    {
      if ($optionsTypes =~ /^(date-picker|datePicker)$/)
      {
        $jscript .= qq{"fixupISODate(field.form.$name" + "_" + destRow + "_" + j + ", '-', '" + d.getFullYear() + "')"};
      }
      elsif ($optionsTypes =~ /^(color-picker|colorPicker)$/)
      {
        $jscript .= qq{"isValidColor(field.form.$name" + "_" + destRow + "_" + j + ")"};
      }
      else
      {
        $jscript .= qq{""};
      }
    }

    $jscript .= qq(;
    if (t2.length > 0)
    {
      if ($jscriptDebugLevel > 0)
        alert("$name - $jscriptPopulateRow():\\n" + t2.toString());
      try {
        eval(t2.toString());
      }
      catch (e)
      {
        alert("$name - $jscriptPopulateRow():\\n" + e + "\\n" + t.toString());
        return 0;
      }
    }
  }

  return 1;  // signal all ok.
}

function $jscriptMoveRow(field, srcRow, destRow)
{
  if (srcRow < 0 || srcRow > $rows)
  {
    alert("$name - $jscriptMoveRow():\\nsrcRow = '" + srcRow + "' is out of bounds!");
    return;
  }

  if (destRow < 0 || destRow > $rows)
  {
    alert("$name - $jscriptMoveRow():\\ndestRow = '" + destRow + "' is out of bounds!");
    return;
  }

  var result = $jscriptPopulateRow(field, srcRow, destRow, 'form');
  if (result)
  {
    // now clear the srcRow
    $jscriptClearRow(field, srcRow);
  }
}

function $jscriptConsolidateRows(field)
{
  for (var i=0; i < $rows; i++)
  {
    if ($jscriptFindNextRow(field, true, i) == i)
    {
      // look for a non-empty row that is above this row (increasing numerically).
      var k = $jscriptFindNextRow(field, false, i+1);
      if (k > i)
      {
        // we found a non-empty row, so move it.
        $jscriptMoveRow(field, k, i);
      }
      else
      {
        return;  // we are done, since there are no more empty rows to consolidate up.
      }
    }
  }
}

function $jscriptEnforceRequiredRows(field)
{
  // first we need to consolidate the rows, just to make life easier.
  $jscriptConsolidateRows(field);

  // now check to make sure the number of required rows have been met.
  if ($required > 0)
  {
    var numFilledRows = $jscriptCountPopulatedRows(field);
    if (numFilledRows < $required)
    {
      alert("$name - $jscriptEnforceRequiredRows():\\n" + "You only have '" + numFilledRows + "' rows filled!\\n$required are required to be filled.");
      return false; // abort the submit.
    }
  }

  return true;  // go ahead and do the submit.
}

);

    # update the result hash.
    $result{html}->{javascript} = $jscript;
    $result{html}->{onsubmit} = qq{if (!$jscriptEnforceRequiredRows(this.$name} . "Select)) { return false; }";
  }

  # now start building up the html, data and profile outputs.
  if (!$manual)
  {
    # build up the html template snippet.
    my $css = ($tableClass ? qq{class="$tableClass" } : "") . ($tableStyle ? qq{style="$tableStyle"} : "");

    # build up the left and right cell contents.
    my $leftCellContent = "";
    my $rightCellContent = "";
    my $selectTemplate = qq{#F=$name} . "Select#";
    my $rowsTemplate = qq{<table $css width="100%" cellpadding="2" cellspacing="0">\n};

    if ($displayColumnHeaders)
    {
      $rowsTemplate .= "  <tr>\n    ";
      for (my $j=0; $j < $numColumns; $j++)
      {
        $rowsTemplate .= qq{<th align="center" colspan="2">};
        my $fname = $name . "_0" . "_$j";
        if (ref ($columnHeaders) eq "ARRAY")
        {
          $rowsTemplate .= $columnHeaders->[$j];
        }
        else
        {
          $rowsTemplate .= "#" . (ref ($optionsTypes) eq "ARRAY" && $optionsTypes->[$j]->{-Type} =~ /^(date|color)(-p|P)icker$/ ? "H" : ($optionsTypes =~ /^(date|color)(-p|P)icker$/ ? "H" : "L")) . "=$fname#";
        }
        $rowsTemplate .= "</th>";
      }
      $rowsTemplate .= "  </tr>\n";
    }

    for (my $i=0; $i < $rows; $i++)
    {
      my $rowClass = ($i % 2 == 0 ? "evenRow" : "oddRow");
      $rowsTemplate .= qq{        <tr class="$rowClass">\n};
      my $tempTemplate = $htmlTemplate;
      for (my $j=0; $j < $numColumns; $j++)
      {
        my $fname = $name . "_$i" . "_$j";
        if ($tempTemplate)
        {
          $tempTemplate =~ s/field_x_$j/$fname/g;
        }
        else
        {
          $rowsTemplate .= qq{          <td align="right" valign="top">#R} . ($displayLabels ? "L" : (ref ($optionsTypes) eq "ARRAY" && $optionsTypes->[$j]->{-Type} =~ /^(date|color)(-p|P)icker$/ ? "L" : ($optionsTypes =~ /^(date|color)(-p|P)icker$/ ? "L" : ""))) . qq{=$fname#</td>\n};
          $rowsTemplate .= qq{          <td align="left">#FI=$fname#</td>\n};
        }
      }
      if ($tempTemplate)
      {
        $tempTemplate =~ s/^(.+)$/          $1/mg;
        $rowsTemplate .= $tempTemplate . ($tempTemplate !~ /\n$/ ? "\n" : "");
      }
      if (!$readOnly)
      {
        $rowsTemplate .= "          <td>#F=$name" . "ClearRow_$i#</td>\n";
      }
      else
      {
        $rowsTemplate .= "          <td><b>Clear</b></td>\n";
      }
      $rowsTemplate .= "        </tr>\n";
    }
    $rowsTemplate .= "      </table>";

    if ($selectLocation eq "left")
    {
      $leftCellContent = $selectTemplate;
      $rightCellContent = $rowsTemplate;
    }
    else
    {
      $leftCellContent = $rowsTemplate;
      $rightCellContent = $selectTemplate;
    }

    $css = ($tableClass ? qq{class="$tableClass" } : "") . qq{style="border-collapse: collapse; border: solid 1pt;} . ($tableStyle ? " " . $tableStyle : "") . qq{"};
    my $body = qq{
<table $css width="100%" cellpadding="2" cellspacing="0">
  <tr>
    <td align="center" style="border: solid 1pt;">
      $leftCellContent
    </td>
    <td align="center" style="border: solid 1pt;">
      $rightCellContent
    </td>
  </tr>
</table>
};
    $result{html}->{body} = $body;
    $result{html}->{link} = "<link href=\"/htmlobject/css/htmlobject.css\" rel=\"stylesheet\" type=\"text/css\" />";

    # build up the data hash, first the select box.
    my %selectOptions = ();
    for (my $i=0; $i < scalar @{$options}; $i++)
    {
      push @{$selectOptions{names}}, $options->[$i]->{label};
      push @{$selectOptions{values}}, $i; # the value is not actually used by the widget.
    }

    my %css = ();
    $css{class} = $selectClass if ($selectClass);
    $css{style} = $selectStyle if ($selectStyle);

    $result{data}->{$name . "Select"} = { -Type => "select", -Value => "", -Label => "", -Options => \%selectOptions, %css,
                                          onchange => "$jscriptSelect(this)", -onload => "this.selectedIndex = -1;",
                                          size => $numSelectRows, %readOnlyArgs };
    push @{$result{order}}, $name . "Select";

    # now generate the rows.
    for (my $i=0; $i < $rows; $i++)
    {
      # make the Clear button
      $result{data}->{$name."ClearRow_$i"} = { -Type => "button", -Value => $clearPhrase, onclick => "$jscriptClearRow(this, $i);" } if (!$readOnly);

      for (my $j=0; $j < $numColumns; $j++)
      {
        my $fname = $name . "_$i" . "_$j";
        $result{data}->{$fname} = { %readOnlyArgs };
        if (ref ($optionsTypes) eq "ARRAY")
        {
          # clone the optionsTypes entry for this column.
          foreach my $entry (keys %{$optionsTypes->[$j]})
          {
            $result{data}->{$fname}->{$entry} = $optionsTypes->[$j]->{$entry};
          }
          if ($result{data}->{$fname}->{-Type} =~ /^(select|multi-select)$/)
          {
            $result{data}->{$fname}->{-NoSelectByDefault} = 1;
          }
          if ($result{data}->{$fname}->{-Type} =~ /^(date-picker|color-picker|datePicker|colorPicker)$/ && $readOnly)
          {
            # don't generate the < > date links when read-only.
            $result{data}->{$fname}->{-displayPrevNextDateLinks} = 0;
          }
          if ($result{data}->{$fname}->{-Type} =~ /^(searchBox)$/ && $readOnly)
          {
            $result{data}->{$fname}->{-Type} = "text";  # otherwise the searchBox will not be made read-only, etc.
          }
        }
        else
        {
          $result{data}->{$fname}->{-Type} = $optionsTypes;
          $result{data}->{$fname}->{-Value} = "";
          $result{data}->{$fname}->{-Label} = $customLabel;
          if ($optionsTypes =~ /^(date-picker|color-picker|datePicker|colorPicker)$/ && $readOnly)
          {
            # don't generate the < > date links when read-only.
            $result{data}->{$fname}->{-displayPrevNextDateLinks} = 0;
          }
        }
        push @{$result{order}}, $fname;
      }
      push @{$result{order}}, $name."ClearRow_$i" if (!$readOnly);
    }

    # build up the profile structure.
    if (!$readOnly)
    {
      for (my $i=0; $i < $required; $i++)
      {
        for (my $j=0; $j < $numColumns; $j++)
        {
          my $fname = $name . "_$i" . "_$j";
          push @{$result{profile}->{required}}, $fname;

          # handle any special checks here
        }
      }
    }
  }

  return %result;
}

=item % generateSearchBox(form, name, label, class, data, shareData, isSorted, isAA, displayEmpty, displayHelp, onblur, onfocus)

 This is called from HTMLObject::Form->generate() for -Type = searchBox.

 requires:
   form - name of the form we are working with.
   name - name of the input field we are creating.
   label - string to display as the label for the input field.
     Defaults to ''.
   class - string to use to specify the class the label and
     input field should be part of.  Defaults to ''.
   isSorted - boolean. 1 (true) means the data should be
     sorted, 0 (false) means it is not sorted.
     Defaults to 0 (false).
   displayEmpty - boolean.  1 (true) means to display all
     available options when the input field is empty,
     else nothing is displayed.
     Defaults to 1 (true).
   displayHelp - boolean.  1 (true) generates a help link
     and allows the widget to process the user interaction
     with the help system.
     Defaults to 1 (true).
   onblur - string of code to be run when the focus is
     moving to another form item for real.
     Defaults to ''.
   onfocus - string of code to be run when the form item gets
     initial focus.  Defaults to ''.

 optional:
   data - array of values to let the user work with or hash
     of values where the key is displayed to the user and
     the keys value is set in the form item.  Defaults to undef.
   shareData - name of the javascript array to use.  If specified,
     then data will be ignored.  Defaults to ''.
   isAA - boolean.  1 (true) means the shareData javascript array
     is an associative array.  0 (false) means it is a normal array.
     Only valid if shareData is specified.  Defaults to 0.

 You must specify one of data or shareData.

 returns:
   hash with following entries (html, data, profile):
     html is a hash with (body, javascript, onload, javascriptIncludes)
       body - html template replacement for the generate() method.
         The #LFI=x# where x = -name will be replaced with this
         string, to allow for generating the hidden form item
         when -data is a hash.  The output will be either
         #LFI=x# or #F=x##LFI=y#, where x = -name and
         y = -name + 'Display'.
       javascript - javascript code needed for this widget to work.
       onload - javascript code needed to initialize the widget.
       javascriptIncludes - points to the search-methods.js library.
     data - generated data hash that will be used by the generate()
       method to actually create the necessary form items.  This will
       just specify the -Type and name, etc. of the form items.
       It will be upto the caller to add any extra things like
       size, disabled, etc.  Do not specify an onfocus or onblur
       handler directly on the resulting form item, otherwise they
       will not be processed.

 summary:
   This method will take the data you specified and setup the necessary
   javascript data structures to allow the searchBox code to do its
   thing.

   If you specified a hash for data, then 2 form items will be
   generated.  The item you named will be created as a hidden input
   field.  A text input field will then be created named
   name + 'Display', where name is the value you specified.  This
   input field will be what the searchBox uses to interact with the
   user and the hidden field is what the users end selection will be
   placed in.  The profile will only require the hidden field to be
   specified.  If you specify isSorted = 1 (true), then the
   javascript code will sort the keys after creating an array from
   them, otherwise your data is left in the order it is processed.

   If you specified an array for data, then only an input field will
   be created and no extra work will need to be done.  The input the
   user selected/typed will be what is returned to the server.

=cut
sub generateSearchBox
{
  my $self = shift;
  my %args = ( form => "", name => "", data => undef, shareData => "", isSorted => 0, isAA => 0, displayEmpty => 1,
               displayHelp => 1, onblur => "", onfocus => "", label => "", class => "", @_ );
  my $form = $args{form};
  my $name = $args{name};
  my $label = $args{label};
  my $class = $args{class};
  my $data = $args{data};
  my $shareData = $args{shareData};
  my $isSorted = $args{isSorted};
  my $isAA = $args{isAA};
  my $displayEmpty = $args{displayEmpty};
  my $displayHelp = $args{displayHelp};
  my $onblur = $args{onblur};
  my $onfocus = $args{onfocus};

  my %result = ( html => { body => "", javascript => "", onload => "", link => "" }, data => {} );

  # validate the input
  if ($form eq "")
  {
    $self->missing("form");
  }
  if ($name eq "")
  {
    $self->missing("name");
  }
  if (length $shareData > 0)
  {
    if ($isAA !~ /^(0|1)$/)
    {
      $self->invalid("isAA", $isAA, "must be boolean (1 or 0)");
    }
  }
  elsif (!defined $data)
  {
    $self->missing("data");
  }
  elsif (ref($data) !~ /^(ARRAY|HASH)$/)
  {
    $self->invalid("ref(data)", ref($data), "must be array or hash");
  }
  if ($isSorted !~ /^(0|1)$/)
  {
    $self->invalid("isSorted", $isSorted, "must be boolean (1 or 0)");
  }
  if ($displayEmpty !~ /^(0|1)$/)
  {
    $self->invalid("displayEmpty", $displayEmpty, "must be boolean (1 or 0)");
  }
  if ($displayHelp !~ /^(0|1)$/)
  {
    $self->invalid("displayHelp", $displayHelp, "must be boolean (1 or 0)");
  }
  if ($self->numInvalid() > 0 || $self->numMissing() > 0)
  {
    $self->error($self->genErrorString("all"));
    return %result;
  }

  # convert to javascript boolean values.
  $isSorted = ($isSorted ? "true" : "false");
  $displayEmpty = ($displayEmpty ? "true" : "false");
  $displayHelp = ($displayHelp ? "true" : "false");

  # now begin the process of generating the javascript data structures.
  my $jscript = "\n";
  my $jscriptData = $name . "SearchItems";
  my $p = $name;
  my $ph = "null";
  $isAA = (length $shareData > 0 ? ($isAA ? "true" : "false") : "false");
  if (length $shareData > 0)
  {
    if ($isAA eq "true")
    {
      $p .= "Display";
      $ph = "document.$form.$name";
    }
    $jscriptData = $shareData;
  }
  elsif (ref($data) eq "ARRAY")
  {
    $jscript .= "var $jscriptData = ['" . join("', '", @{$data}) . "'];\n";
    if ($isSorted eq "true")
    {
      $jscript .= "$jscriptData = $jscriptData.sort();\n";
    }
  }
  else
  {
    $isAA = "true";
    $p .= "Display";
    $ph = "document.$form.$name";
    $jscript .= "var $jscriptData = new Array();\n";
    foreach my $key (keys %{$data})
    {
      $jscript .= "$jscriptData" . "['$key'] = '$data->{$key}';\n";
    }
  }

  $result{html}->{javascript} = $jscript;
  $result{html}->{onload} = "searchBox(document.$form.$p, $ph, $jscriptData, $isSorted, $isAA, $displayEmpty, $displayHelp, '$onfocus', '$onblur');\n";
  $result{html}->{javascriptIncludes} = "<script language=\"JavaScript1.5\" src=\"/searchbox/search-methods.js\" type=\"text/javascript\"></script>";

  # now build up the body template string and the data structures.
  $result{html}->{body} .= "#F=" . ($ph eq "null" ? $p . "#" : "$p##F=$name#");

  $result{data}->{$p} = { -Type => "text", -Label => $label, -Value => "", class => $class };
  if ($ph ne "null")
  {
    $result{data}->{$name} = { -Type => "hidden", -Value => "" };
  }

  return %result;
}

=back

=cut

1;
__END__

=head1 AUTHOR

James A. Pattie, htmlobject@pcxperience.com

=head1 SEE ALSO

perl(1), HTMLObject::Base(3), HTMLObject::FrameSet(3), HTMLObject::ReadCookie(3), HTMLObject::Form(3), HTMLObject::ErrorBase(3).

=cut
