# 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.28';
=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)
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 the phrase displayed in.
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, @_ );
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 $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 ? "" : "") . ($class ? "" : "") . $phrase . ($class ? "" : "") . ($link ? "" : "");
my $inputStr = ($type eq "date" && $displayPrevNextDateLinks ? qq{< } : "") . "" . ($type eq "date" && $displayPrevNextDateLinks ? qq{ >} : "");
$result{body} = $linkStr . " " . $inputStr;
$result{_link_} = $linkStr;
$result{_input_} = $inputStr;
$result{javascriptIncludes} = "";
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
cells
and not the parent
cell.
Ex:
htmlTemplate = "
#LRFI=field_x_0#
#LRFI=field_x_1#
"
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 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
tags.
If htmlTemplate is not specified, then each form item is defined as:
#RL=field_x_y#
#FI=field_x_y#
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{
\n};
if ($displayColumnHeaders)
{
$rowsTemplate .= "
};
$result{html}->{body} = $body;
$result{html}->{link} = "";
# 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} = "";
# 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