# 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 .= " \n "; for (my $j=0; $j < $numColumns; $j++) { $rowsTemplate .= qq{"; } $rowsTemplate .= " \n"; } for (my $i=0; $i < $rows; $i++) { my $rowClass = ($i % 2 == 0 ? "evenRow" : "oddRow"); $rowsTemplate .= qq{ \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{ \n}; $rowsTemplate .= qq{ \n}; } } if ($tempTemplate) { $tempTemplate =~ s/^(.+)$/ $1/mg; $rowsTemplate .= $tempTemplate . ($tempTemplate !~ /\n$/ ? "\n" : ""); } if (!$readOnly) { $rowsTemplate .= " \n"; } else { $rowsTemplate .= " \n"; } $rowsTemplate .= " \n"; } $rowsTemplate .= "
}; 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 .= "
#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##FI=$fname##F=$name" . "ClearRow_$i#Clear
"; 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{
$leftCellContent $rightCellContent
}; $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