1 # Vend::Form - Generate Form widgets
3 # $Id: Form.pm,v 2.77 2009-04-05 19:24:36 mheins Exp $
5 # Copyright (C) 2002-2008 Interchange Development Group
6 # Copyright (C) 1996-2002 Red Hat, Inc.
8 # This program was originally based on Vend 0.2 and 0.3
9 # Copyright 1995 by Andrew M. Wilcox <amw@wilcoxsolutions.com>
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public
22 # License along with this program; if not, write to the Free
23 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
28 require HTML::Entities;
29 *encode = \&HTML::Entities::encode_entities;
30 use Vend::Interpolate;
34 no warnings qw(uninitialized numeric);
35 use POSIX qw{strftime};
37 use vars qw/@ISA @EXPORT @EXPORT_OK $VERSION %Template %ExtraMeta/;
42 $VERSION = substr(q$Revision: 2.77 $, 10);
50 Vend::Form -- Interchange form element routines
58 Provides form element routines for Interchange, emulating the old
59 tag_accessories stuff. Allows user-added widgets.
65 my $Some = '(?s:.)*?';
66 my $Codere = '[-\w#/.]+';
67 my $Tag = new Vend::Tags;
71 qq({PREPEND}{VALUE}{APPEND})
74 qq({PREPEND}<select name="{NAME}")
76 qq({ROWS?} size="{ROWS}"{/ROWS?})
78 qq({DISABLED?} disabled{/DISABLED?})
80 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
82 qq({MULTIPLE?} multiple{/MULTIPLE?})
84 qq({EXTRA?} {EXTRA}{/EXTRA?})
94 qq(<textarea name="{NAME}")
96 qq({ROWS?} rows="{ROWS}"{/ROWS?})
98 qq({COLS?} cols="{COLS}"{/COLS?})
100 qq({DISABLED?} disabled{/DISABLED?})
102 qq({MAXLENGTH?} maxlength="{MAXLENGTH}"{/MAXLENGTH?})
104 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
106 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
108 qq({WRAP?} wrap="{WRAP}"{/WRAP?})
110 qq({EXTRA?} {EXTRA}{/EXTRA?})
112 qq(>{ENCODED}</textarea>)
117 qq({PREPEND}<input type="password" name="{NAME}" value="{ENCODED}")
119 qq({COLS?} size="{COLS}"{/COLS?})
121 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
123 qq({MAXLENGTH?} maxlength="{MAXLENGTH}"{/MAXLENGTH?})
125 qq({EXTRA?} {EXTRA}{/EXTRA?})
130 qq({PREPEND}<input type="file" name="{NAME}" value="{ENCODED}")
132 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
134 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
136 qq({COLS?} size="{COLS}"{/COLS?})
138 qq({EXTRA?} {EXTRA}{/EXTRA?})
143 qq({PREPEND}<input type="file" name="{NAME}" value="{ENCODED}")
145 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
147 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
149 qq({COLS?} size="{COLS}"{/COLS?})
151 qq({EXTRA?} {EXTRA}{/EXTRA?})
153 qq(><br{XTRAILER}><textarea cols="{WIDTH}" rows="{HEIGHT}" name="{NAME}">{ENCODED}</textarea>{APPEND})
156 qq({PREPEND}<input type="text" name="{NAME}" value="{ENCODED}")
158 qq({COLS?} size="{COLS}"{/COLS?})
160 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
162 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
164 qq({DISABLED?} disabled{/DISABLED?})
166 qq({MAXLENGTH?} maxlength="{MAXLENGTH}"{/MAXLENGTH?})
168 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
170 qq({EXTRA?} {EXTRA}{/EXTRA?})
175 qq({PREPEND}<input type="hidden" name="{NAME}" value="{ENCODED}")
177 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
179 qq({EXTRA?} {EXTRA}{/EXTRA?})
184 qq({PREPEND}<input type="hidden" name="{NAME}" value="{ENCODED}")
186 qq({EXTRA?} {EXTRA}{/EXTRA?})
188 qq(>{FILTERED?}{FILTERED}{/FILTERED?}{FILTERED:}{ENCODED}{/FILTERED:}{APPEND})
191 qq(<input type="{VARIANT}" name="{NAME}" value="{TVALUE}")
193 qq({EXTRA?} {EXTRA}{/EXTRA?})
195 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
197 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
199 qq({DISABLED?} disabled{/DISABLED?})
201 qq({SELECTED?} checked{/SELECTED?})
203 qq(> {TTITLE?}<span title="{TTITLE}">{/TTITLE?}{TEXTID?}<label for="{TEXTID}">{/TEXTID?}{TLABEL}{TEXTID?}</label>{/TEXTID?}{TTITLE?}</span>{/TTITLE?})
206 qq(<input type="{VARIANT}" name="{NAME}" value="{TVALUE}")
208 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
210 qq({EXTRA?} {EXTRA}{/EXTRA?})
212 qq({DISABLED?} disabled{/DISABLED?})
214 qq({SELECTED?} checked{/SELECTED?})
216 qq(> {TTITLE?}<span title="{TTITLE}">{/TTITLE?}{TEXTID?}<label for="{TEXTID}">{/TEXTID?}{TLABEL}{TEXTID?}</label>{/TEXTID?}{TTITLE?}</span>{/TTITLE?} )
219 qq(<td{TD_LABEL?} {TD_LABEL}{/TD_LABEL?}{TTITLE?} title="{TTITLE}"{/TTITLE?}>)
221 qq({FONT?}<font size="{FONT}">{/FONT?})
223 qq({TEXTID?}<label for="{TEXTID}">{/TEXTID?}{TLABEL}{TEXTID?}</label>{/TEXTID?}{FONT?}</font>{/FONT?})
228 qq(<td{TD_VALUE?} {TD_VALUE}{/TD_VALUE?}>)
230 qq(<input type="{VARIANT}" name="{NAME}" value="{TVALUE}")
232 qq({TTITLE?} title="{TTITLE}"{/TTITLE?})
234 qq({TEXTID?} id="{TEXTID}"{/TEXTID?})
236 qq({DISABLED?} disabled{/DISABLED?})
238 qq({EXTRA?} {EXTRA}{/EXTRA?})
240 qq({SELECTED?} checked{/SELECTED?})
247 qq(</tr><tr><td{TD_GROUP?} {TD_GROUP}{/TD_GROUP?} colspan="2">)
255 $Template{default} = $Template{text};
258 my ($body, $hash) = @_;
259 return $body unless ref($hash) eq 'HASH';
261 $body =~ s!\{([A-Z_]+)\}!$hash->{lc $1}!g;
262 $body =~ s!\{([A-Z_]+)\|($Some)\}!$hash->{lc $1} || $2!eg;
263 $body =~ s!\{([A-Z_]+)\s+($Some)\}! $hash->{lc $1} ? $2 : ''!eg;
264 1 while $body =~ s!\{([A-Z_]+)\?\}($Some){/\1\?\}! $hash->{lc $1} ? $2 : ''!eg;
265 1 while $body =~ s!\{([A-Z_]+)\:\}($Some){/\1\:\}! $hash->{lc $1} ? '' : $2!eg;
272 return undef if ! $ary;
275 push @out, join "=", @$_;
277 my $delim = Vend::Interpolate::get_joiner($opt->{delimiter}, ',');
278 return join $delim, @out;
284 my $idx = shift || 0;
285 return undef if ! $ary;
288 @out = map {$_->[$idx]} @$ary;
290 my $delim = Vend::Interpolate::get_joiner($opt->{delimiter}, ',');
291 return join $delim, @out;
295 return show_options($_[0], $_[1], 1);
300 return attr_list($Template{$opt->{type}} || $Template{default}, $opt);
303 ## Retrieve the *first* current label
305 my($opt, $data) = @_;
308 if (defined $opt->{value}) {
309 $val = $opt->{value};
311 elsif(defined $opt->{default}) {
312 $val = $opt->{default};
316 my ($setting, $label) = @$_;
317 $default = $label if $label =~ s/\*$//;
318 return ($label || $setting) if $val eq $setting;
320 return $val || $default;
324 my($opt, $opts) = @_;
326 $opt->{joiner} = Vend::Interpolate::get_joiner($opt->{joiner}, "<br$Vend::Xtrailer>");
327 my $name = $opt->{name};
328 my $default = defined $opt->{value} ? $opt->{value} : $opt->{default};
330 $opt->{extra} = " $opt->{extra}" if $opt->{extra};
332 my $template = $opt->{template} || <<EOF;
333 <a href="{URL}"{EXTRA}>{SELECTED <b>}{LABEL}{SELECTED </b>}</a>
336 my $o_template = $opt->{o_template} || <<EOF;
340 my $href = $opt->{href} || $Global::Variable->{MV_PAGE};
341 $opt->{form} = "mv_action=return" unless $opt->{form};
343 my $no_encode = $opt->{pre_filter} eq 'decode_entities' ? 1 : 0;
347 #warn "iterating links opt $_ = " . uneval_it($_) . "\n";
348 my $attr = { extra => $opt->{extra}};
350 s/\*$// and $attr->{selected} = 1;
352 ($attr->{value},$attr->{label}) = @$_;
353 encode($attr->{label}, $ESCAPE_CHARS::std) unless $no_encode;
354 if($attr->{value} =~ /^\s*\~\~(.*)\~\~\s*$/) {
356 $lab =~ s/"/"/g;
357 $opt->{tvalue} = $lab;
358 $opt->{tlabel} = $lab;
359 push @out, attr_list($o_template, $opt);
363 next if ! $attr->{value} and ! $opt->{empty};
364 if( ! length($attr->{label}) ) {
365 $attr->{label} = $attr->{value} or next;
369 $attr->{selected} = $default eq $attr->{value} ? 1 : '';
372 my $form = $opt->{form};
374 $attr->{label} =~ s/\s/ /g if $opt->{nbsp};
376 $attr->{url} = Vend::Interpolate::tag_area(
380 form => "$name=$attr->{value}\n$opt->{form}",
381 secure => $opt->{secure},
384 push @out, attr_list($template, $attr);
386 return join $opt->{joiner}, @out;
395 (@Years) = ( $t[5] + 1899 .. $t[5] + 1910 );
400 push @Months, [sprintf("%02d", $_), POSIX::strftime("%B", @t)];
404 push @Days, [sprintf("%02d", $_), $_];
408 sub round_to_fifteen {
410 #::logDebug("round_to_fifteen val in=$val");
411 $val = substr($val, 0, 4);
412 $val = "0$val" if length($val) == 3;
413 return '0000' if length($val) < 4;
414 if($val !~ /(00|15|30|45)$/) {
415 my $hr = substr($val, 0, 2);
417 my $min = substr($val, 2, 2);
419 if($min > 45 and $hr < 23) {
438 $val = sprintf('%02d%02d', $hr, $min);
440 #::logDebug("round_to_fifteen val out=$val");
447 my $name = $opt->{name};
448 my $val = $opt->{value};
451 $val = Vend::Interpolate::filter_value('date_change', $val);
454 if($opt->{time} and $opt->{time_adjust} =~ /([-+]?)(\d+)/) {
455 my $sign = $1 || '+';
459 $now += $sign eq '+' ? $adjust : -$adjust;
464 for(qw/ class style extra /) {
465 my $stag = "select_$_";
466 my $otag = "option_$_";
471 $selapp = " $opt->{$stag}";
472 $optapp = " $opt->{$otag}";
475 $selapp = qq{ $_="$opt->{$stag}"};
476 $optapp = qq{ $_="$opt->{$otag}"};
478 $sel_extra .= $opt->{$stag} ? $selapp : '';
479 $opt_extra .= $opt->{$otag} ? $optapp : '';
482 my @t = localtime($now || time);
484 my $out = qq{<select name="$name"$sel_extra>};
487 $out .= qq{<option value="0"$opt_extra>------</option>};
489 # use current time with possible adjustments as default value
490 $t[2]++ if $t[2] < 23;
491 $val = POSIX::strftime("%Y%m%d%H00", @t);
494 $o = qq{<option value="$_->[0]"$opt_extra>} . errmsg($_->[1]) . '</option>';
495 ($out .= $o, next) unless ! $sel and $val;
496 $o =~ s/>/ SELECTED>/ && $sel++
497 if substr($val, 4, 2) eq $_->[0];
501 $out .= qq{</select>};
502 $out .= qq{<input type="hidden" name="$name" value="/">};
503 $out .= qq{<select name="$name"$sel_extra>};
505 $out .= qq{<option value="0"$opt_extra>--</option>};
508 $o = qq{<option value="$_->[0]"$opt_extra>$_->[1]} . '</option>';
509 ($out .= $o, next) unless ! $sel and $val;
510 $o =~ s/>/ SELECTED>/ && $sel++
511 if substr($val, 6, 2) eq $_->[0];
515 $out .= qq{</select>};
516 $out .= qq{<input type="hidden" name="$name" value="/">};
517 $out .= qq{<select name="$name"$sel_extra>};
519 my $cy = $t[5] + 1900;
521 # If year_begin or year_end are /00+/, make current year
522 for(qw/ year_begin year_end /) {
523 if( length($opt->{$_}) > 1 and $opt->{$_} == 0) {
528 if(my $by = $opt->{year_begin} || $::Variable->{UI_DATE_BEGIN}) {
529 my $ey = $opt->{year_end} || $::Variable->{UI_DATE_END} || ($cy + 10);
531 $by = $cy - abs($by);
536 @Years = $by <= $ey ? ($by .. $ey) : reverse ($ey .. $by);
539 $out .= qq{<option value="0000"$opt_extra>----</option>};
542 $o = qq{<option$opt_extra>$_} . '</option>';
543 ($out .= $o, next) unless ! $sel and $val;
544 $o =~ s/>/ SELECTED>/ && $sel++
545 if substr($val, 0, 4) eq $_;
548 $out .= qq{</select>};
549 return $out unless $opt->{time};
551 $val =~ s/^(\d{8})//;
552 # If the date is blank (0000-00-00), treat time of 00:00 as blank,
553 # not midnight, in the option selection below
554 my $blank_time = ($opt->{blank} and $1 !~ /[1-9]/);
556 $val = round_to_fifteen($val);
557 $out .= qq{<input type="hidden" name="$name" value=":">};
558 $out .= qq{<select name="$name"$sel_extra>};
560 $out .= qq{<option value="0"$opt_extra>--:--</option>};
563 my $ampm = defined $opt->{ampm} ? $opt->{ampm} : 1;
566 my %special = qw/ 0 midnight 12 noon /;
570 $opt->{minutes} ||= '';
572 if($opt->{minutes} =~ /half/i) {
575 elsif($opt->{minutes} =~ /hourly/i) {
578 elsif($opt->{minutes} =~ /ten/i) {
579 @min = (0,10,20,30,40,50);
581 elsif($opt->{minutes} =~ /[\0,]/) {
582 @min = grep /^\d+$/ && $_ <= 59, split /[\0,\s]+/, $opt->{minutes};
588 $opt->{start_hour} ||= 0;
589 for(qw/start_hour end_hour/) {
590 $opt->{$_} = int(abs($opt->{$_}));
591 if($opt->{$_} > 23) {
595 $opt->{start_hour} ||= 0;
596 $opt->{end_hour} ||= 23;
598 for my $hr ( $opt->{start_hour} .. $opt->{end_hour} ) {
599 next if defined $opt->{start_hour} and $hr < $opt->{start_hour};
600 next if defined $opt->{end_hour} and $hr > $opt->{end_hour};
601 for my $min ( @min ) {
609 $disp_hour = $hr - 12 unless $hr == 12;
614 if($special{$hr} and $min == 0) {
615 $disp_hour = errmsg($special{$hr});
618 $disp_hour = sprintf("%2d:%02d%s", $disp_hour, $min, $mod);
621 $disp_hour = sprintf("%02d:%02d", $hr, $min);
623 my $time = sprintf "%02d%02d", $hr, $min;
624 $o = sprintf qq{<option value="%s"$opt_extra>%s}, $time, $disp_hour;
625 ($out .= $o, next) unless ! $sel and $val;
626 #::logDebug("prospect=$time actual=$val");
627 $o =~ s/>/ SELECTED>/ && $sel++
628 if ! $blank_time and $val eq $time;
636 sub option_widget_box {
637 my ($name, $val, $lab, $default, $width) = @_;
638 my $half = int($width / 2);
639 my $sel = $default ? ' SELECTED' : '';
640 $val =~ s/"/"/g;
641 $lab =~ s/"/"/g;
642 $width = 10 if ! $width;
643 return qq{<tr><td><small><input type="text" name="$name" value="$val" size="$half"></small></td><td><small><input type="text" name="$name" value="$lab" size="$width"></small></td><td><small><select name="$name"><option value="0">no<option value="1"$sel>default*</select></small></td></tr>};
648 my($name, $val) = ($opt->{name}, $opt->{value});
650 my $width = $opt->{width} || 16;
651 $opt->{filter} = 'option_format'
652 unless length($opt->{filter});
653 $val = Vend::Interpolate::filter_value($opt->{filter}, $val);
654 my @opts = split /\s*,\s*/, $val;
656 my $out = qq{<table cellpadding="0" cellspacing="0"><tr><th><small>};
657 $out .= errmsg('Value');
658 $out .= qq{</small></th><th align="left" colspan="2"><small>};
659 $out .= errmsg('Label');
660 $out .= qq{</small></th></tr>};
663 my $height = $opt->{height} || 5;
666 my ($v,$l) = split /\s*=\s*/, $_, 2;
667 next unless $l || length($v);
670 ($l =~ s/\*$// or ! $l && $v =~ s/\*$//)
672 $out .= option_widget_box($name, $v, $l, $default, $width);
674 while($done++ < $height) {
675 $out .= option_widget_box($name, '', '', '', $width);
677 $out .= option_widget_box($name, '', '', '', $width);
678 $out .= option_widget_box($name, '', '', '', $width);
684 my ($opt, $opts) = @_;
685 my $name = $opt->{name};
686 $opt->{name} = "X$name";
687 my $usenl = $opt->{rows} > 1 ? 1 : 0;
688 my $only = $opt->{replace} ? 1 : 0;
689 $opt->{extra} .= qq{ onChange="addItem(this.form['X$name'],this.form['$name'],$usenl,$only)"}
690 unless $opt->{extra} =~ m/\bonchange\s*=/i;
692 $opt->{rows} = $opt->{height} unless length($opt->{rows});
693 $opt->{cols} = $opt->{width} unless length($opt->{cols});
696 my $out = dropdown($opt, $opts);
698 my $template = $opt->{o_template} || '';
700 if($opt->{rows} > 1) {
701 $template .= q(<textarea rows="{ROWS|4}" wrap="{WRAP|virtual}");
702 $template .= q( cols="{COLS|20}" name="{NAME}">{ENCODED}</textarea>);
705 $template .= qq(<input type="text" size="{COLS||40}");
706 $template .= qq( name="{NAME}" value="{ENCODED}">);
709 $opt->{name} = $name;
710 $tbox = attr_list($template, $opt);
712 return $opt->{reverse} ? $tbox . $out : $out . $tbox;
716 my ($opt, $opts) = @_;
718 if($opt->{textarea}) {
719 my $template = $opt->{o_template};
721 $template = "<br$Vend::Xtrailer>";
722 if(! $opt->{rows} or $opt->{rows} > 1) {
723 $template .= q(<textarea rows="{ROWS|2}" wrap="{WRAP|virtual}");
724 $template .= q( cols="{COLS|60}" name="{NAME}">);
725 $template .= '{ENCODED}'
726 unless $opt->{conditional_text} and length($opt->{value}) < 3;
727 $template .= q(</textarea>);
730 $template .= qq(<input type="text" size="{COLS|40}");
731 $template .= qq( name="{NAME}" value=");
732 $template .= '{ENCODED}'
733 unless $opt->{conditional_text} and length($opt->{value}) < 3;
737 $addl = attr_list($template, $opt);
740 $addl = qq|<input type="text" name="$opt->{name}"|;
741 $addl .= qq| size="$opt->{cols}" value="">|;
743 if($opt->{reverse}) {
744 $opt->{append} = length($opt->{append}) ? "$addl$opt->{append}" : $addl;
747 $opt->{prepend} = length($opt->{prepend}) ? "$opt->{prepend}$addl" : $addl;
749 return dropdown($opt, $opts);
753 my($opt, $opts) = @_;
754 #::logDebug("called select opt=" . ::uneval($opt) . "\nopts=" . ::uneval($opts));
755 $opt->{multiple} = 1 if $opt->{type} eq 'multiple';
759 my $price = $opt->{price} || {};
762 #::logDebug("template for selecthead: $Template{selecthead}");
763 #::logDebug("opt is " . ::uneval($opt));
764 my $run = attr_list($Template{selecthead}, $opt);
765 #::logDebug("run is now: $run");
766 my ($multi, $re_b, $re_e, $regex);
767 #::logDebug("select multiple=$opt->{multiple}");
768 if($opt->{multiple}) {
770 if($opt->{rawvalue}) {
775 $re_b = '(?:[\0,\s]|^)';
776 $re_e = '(?:[\0,\s]|$)';
786 my $cols = $opt->{cols};
788 return $_[0] if length($_[0]) <= $cols;
789 return substr($_[0], 0, $cols - 2) . '..';
793 $limit = sub { return $_[0] };
796 my $default = $opt->{value};
799 my $no_encode = $opt->{pre_filter} eq 'decode_entities' ? 1 : 0;
802 my ($value, $label, $help) = @$_;
803 encode($label, $ESCAPE_CHARS::std) unless $no_encode;
804 encode($help, $ESCAPE_CHARS::std) if $help;
805 if($value =~ /^\s*\~\~(.*)\~\~\s*$/) {
807 if($optgroup_one++) {
808 $run .= "</optgroup>";
810 $run .= qq{<optgroup label="$label">};
817 $label =~ s/\*$// and $select = 1;
820 $value =~ s/\*$// and $select = 1;
823 $select = '' if defined $default;
827 if(my $p = $price->{$value}) {
828 $attr->{negative} = $p < 0 ? 1 : 0;
829 $attr->{price_noformat} = $p;
830 $attr->{absolute} = currency(abs($p), undef, 1);
831 $attr->{price} = $extra = currency($p, undef, 1);
832 $extra = " ($extra)";
836 encode($vvalue, $ESCAPE_CHARS::std);
837 $run .= qq| value="$vvalue"|;
838 $run .= qq| title="$help"| if $help;
839 if (length($default)) {
840 $regex = qr/$re_b\Q$value\E$re_e/;
841 $default =~ $regex and $select = 1;
842 } elsif (defined($default) && length($value) == 0) {
845 $run .= ' SELECTED' if $select;
847 if($opt->{option_template}) {
848 $attr->{label} = $label || $value;
849 $attr->{value} = $value;
850 $run .= attr_list($opt->{option_template}, $attr);
853 $run .= $limit->($label);
857 $run .= $limit->($value);
861 $run .= "</optgroup>" if $optgroup_one++;
862 $run .= attr_list($Template{selecttail}, $opt);
867 Provides an easy "Yes/No" widget. C<No> returns a value of blank/false,
868 and C<Yes> returns 1/true.
873 name => 'varname' || undef, ## Derived from item if called by
874 # [PREFIX-options] or [PREFIX-accessories]
875 type => 'yesno' || 'yesno radio', ## Second is shorthand for variant=>radio
876 variant => 'radio' || 'select', ## Default is select
879 The data array passed by C<passed> is never used, it is overwritten
880 with the equivalent of '=No,1=Yes'. C<No> and C<Yes> are generated from
881 the locale, so if you want a translated version set those keys in the locale.
883 If you want another behavior the same widget can be constructed with:
885 [display passed="=My no,0=My yes" type=select ...]
892 $opt->{value} = is_yes($opt->{value});
893 my $yes = defined $opt->{yes_value} ? $opt->{yes_value} : 1;
894 my $no = defined $opt->{no_value} ? $opt->{no_value} : '';
895 my $yes_title = defined $opt->{yes_title} ? $opt->{yes_title} : errmsg('Yes');
896 my $no_title = defined $opt->{no_title} ? $opt->{no_title} : errmsg('No');
898 my $routine = $opt->{subwidget} || \&dropdown;
899 if($opt->{variant} eq 'checkbox') {
908 return $routine->($opt, \@opts);
913 Same as C<yesno> except sense is reversed. C<No> returns a value of 1/true,
914 and C<Yes> returns blank/false.
920 $opt->{value} = is_no($opt->{value});
925 my $routine = $opt->{subwidget} || \&dropdown;
926 return $routine->($opt, \@opts);
930 my($opt, $opts) = @_;
931 #::logDebug("Called box type=$opt->{type}");
932 my $inc = $opt->{breakmod};
933 my ($xlt, $template, $o_template, $header, $footer, $row_hdr, $row_ftr);
935 $opt->{variant} ||= $opt->{type};
937 $header = $template = $footer = $row_hdr = $row_ftr = '';
941 $template = $Template{boxnbsp};
943 elsif ($opt->{left}) {
945 $footer = '</table>';
946 $template = '<tr>' unless $inc;
947 $template .= $Template{boxvalue};
948 $template .= $Template{boxlabel};
949 $template .= '</tr>' unless $inc;
950 $o_template = $Template{boxgroup};
952 elsif ($opt->{right}) {
954 $footer = '</table>';
955 $template = '<tr>' unless $inc;
956 $template .= $Template{boxlabel};
957 $template .= $Template{boxvalue};
958 $template .= '</tr>' unless $inc;
959 $o_template = $Template{boxgroup};
962 $template = $Template{boxstd};
964 $o_template ||= "<br$Vend::Xtrailer><b>{TVALUE}</b><br$Vend::Xtrailer>";
968 my $price = $opt->{price} || {};
971 my $default = $opt->{value};
972 my $no_encode = $opt->{pre_filter} eq 'decode_entities' ? 1 : 0;
975 my($value,$label,$help) = @$_;
976 encode($label, $ESCAPE_CHARS::std) unless $no_encode;
977 encode($help, $ESCAPE_CHARS::std) if $help;
978 if($value =~ /^\s*\~\~(.*)\~\~\s*$/) {
980 $lab =~ s/"/"/g;
981 $opt->{tvalue} = $lab;
982 $opt->{tlabel} = $lab;
983 $run .= attr_list($o_template, $opt);
987 $value = '' if ! length($value);
988 $label = $value if ! length($label);
990 $run .= '<tr>' if $inc && ! ($i % $inc);
993 undef $opt->{selected};
995 and $opt->{selected} = 1;
996 $opt->{selected} = '' if defined $opt->{value};
999 my $attr = { label => $label, value => $value };
1000 if(my $p = $price->{$value}) {
1001 $attr->{negative} = $p < 0 ? 1 : 0;
1002 $attr->{price_noformat} = $p;
1003 $attr->{absolute} = currency(abs($p), undef, 1);
1004 $attr->{price} = $extra = currency($p, undef, 1);
1005 $label .= " ($attr->{price})";
1009 and defined $default
1011 and $opt->{selected} = 1;
1014 my $regex = $opt->{contains}
1016 : qr/\b\Q$value\E\b/;
1017 $default =~ $regex and $opt->{selected} = 1;
1020 $opt->{tvalue} = encode($value, $ESCAPE_CHARS::std);
1022 if($opt->{option_template}) {
1023 $opt->{tlabel} = attr_list($opt->{option_template}, $attr);
1024 $opt->{tlabel} =~ s/ / /g if $xlt;
1027 $label =~ s/ / /g if $xlt;
1028 $opt->{tlabel} = $label;
1031 $opt->{ttitle} = $help;
1034 $opt->{textid} = $opt->{id} . ($value eq '' ? 0 : $value);
1035 $opt->{textid} =~ s/[^-\w]+//g;
1038 $run .= attr_list($template, $opt);
1039 $run .= '</tr>' if $inc && ! ($i % $inc);
1044 sub options_to_array {
1045 my ($passed, $opt) = @_;
1046 return $passed if ref($passed) eq 'ARRAY'
1050 ref($passed->[0]) eq 'ARRAY'
1056 if($passed =~ m{^[^=]*\0}) {
1057 $passed = Vend::Interpolate::filter_value($passed, 'option_format');
1060 my $delim = $opt->{delimiter} || ',';
1061 $delim = '\s*' . $delim . '\s*';
1063 if (ref $passed eq 'SCALAR') {
1064 $passed = [ split /$delim/, $$passed ];
1066 elsif(! ref $passed) {
1067 $passed = [ split /$delim/, $passed ];
1070 if (ref $passed eq 'ARRAY') {
1072 push @out, [split /\s*=\s*/, HTML::Entities::decode($_), 2];
1075 elsif (ref $passed eq 'HASH') {
1078 my $nsub = sub { ($_->{$a} || $a) <=> ($_->{$b} || $b) };
1079 my $asub = sub { ($_->{$a} || $a) cmp ($_->{$b} || $b) };
1080 if(! $opt->{sort_option}) {
1083 elsif($opt->{sort_option} eq 'none') {
1086 elsif($opt->{sort_option} =~ /n/i) {
1093 @keys = $sub ? (sort $sub keys %$passed) : (keys %$passed);
1096 push @out, [$_, $passed->{$_}];
1100 die "bad data type to options_to_array";
1103 if ($opt->{applylocale}) {
1105 $_->[1] = errmsg($_->[1]);
1113 my($opt, $item, $data) = @_;
1116 ::logDebug("display called, options=" . uneval($opt));
1117 ::logDebug("item=" . uneval($item)) if $item;
1121 ### Has effect of simple default widget for name
1122 ### or some text output
1123 if($opt =~ /^$Codere$/) {
1124 $opt = { name => $opt };
1130 elsif (ref $opt eq 'ARRAY') {
1131 ### Handle multiple things passed
1134 push @out, display( ref $_ eq 'ARRAY' ? @$_ : ($_));
1136 return join "", @out;
1139 if($opt->{override}) {
1140 $opt->{value} = $opt->{default};
1143 $opt->{default} = $opt->{value} if defined $opt->{value};
1145 if($opt->{pre_filter} and defined $opt->{value}) {
1146 $opt->{value} = Vend::Interpolate::filter_value(
1153 if(ref ($item) eq 'HASH') {
1154 #::logDebug("item=$item");
1158 $item = get_option_hash($item || $opt->{item});
1160 #::logDebug("item=" . ::uneval($item));
1166 ## Set some defaults, can't have attribute or type '0';
1167 ## Note the fact that attribute can take its value from name
1169 $opt->{attribute} ||= $opt->{name};
1170 $opt->{prepend} = '' unless defined $opt->{prepend};
1171 $opt->{append} = '' unless defined $opt->{append};
1172 $opt->{delimiter} = ',' unless length($opt->{delimiter});
1173 $opt->{cols} ||= $opt->{width} || $opt->{size};
1174 $opt->{rows} ||= $opt->{height};
1176 if($opt->{js_check}) {
1177 my @checks = grep /\w/, split /[\s,\0]+/, $opt->{js_check};
1179 if(my $sub = Vend::Util::codedef_routine('JavaScriptCheck', $_)) {
1183 ::logError('Unknown %s: %s', 'JavaScriptCheck', $_);
1188 # This handles the embedded attribute information in certain types,
1191 # text_60 is the same as type => 'text', width => '60'
1192 # datetime_ampm is the same as type => 'datetime', ampm => 1
1194 # Warning -- this sets $opt->{type} and has possible side-effects
1196 my $type = parse_type($opt);
1198 #::logDebug("name=$opt->{name} type=$type");
1202 if($look = $opt->{lookup_query}) {
1203 #::logDebug("lookup_query called, opt=" . uneval($opt));
1204 my $tab = $opt->{db} || $opt->{table} || $Vend::Cfg->{ProductFiles}[0];
1205 my $db = Vend::Data::database_exists_ref($tab);
1206 my @looks = split /\s*;\s*/, $look;
1208 for my $l (@looks) {
1210 next unless $l =~ /^\s*select\s+/i;
1211 my $qr = $db->query($l);
1212 ref($qr) eq 'ARRAY' and push @$data, @$qr;
1214 if($data->[0] and @{$data->[0]} > 2) {
1215 my $j = $opt->{label_joiner} || '-';
1217 $_->[1] = join $j, splice @$_, 1;
1221 elsif($look = $opt->{lookup}) {
1222 #::logDebug("lookup called, opt=" . uneval($opt));
1224 my $tab = $opt->{db} || $opt->{table} || $Vend::Cfg->{ProductFiles}[0];
1225 my $db = Vend::Data::database_exists_ref($tab)
1227 my $fld = $opt->{field} || $look;
1230 if($key ne $fld and $fld !~ /,/) {
1234 my @f = split /\s*,\s*/, $fld;
1235 my $order = $opt->{sort} || $f[1] || $f[0];
1236 last LOOK unless $tab;
1237 my $q = qq{SELECT DISTINCT $fld FROM $tab ORDER BY $order};
1239 $data = $db->query($q) || die;
1242 my $join = $opt->{label_joiner} || '-';
1243 my $string = join $join, splice @$_, 1;
1250 elsif($opt->{passed}) {
1251 $data = options_to_array($opt->{passed}, $opt);
1253 elsif(! $opt->{already_got_data} and $opt->{column} and $opt->{table} ) {
1255 last GETDATA if $opt->{table} eq 'mv_null';
1256 my $key = $opt->{outboard} || $item->{code} || $opt->{code};
1257 last GETDATA unless length($key);
1258 last GETDATA unless ::database_exists_ref($opt->{table});
1259 $opt->{passed} = $Tag->data($opt->{table}, $opt->{column}, $key)
1261 $data = options_to_array($opt->{passed}, $opt);
1265 ## This means a lookup was attempted above
1266 if($look and $data) {
1268 if($opt->{options}) {
1269 $ary = options_to_array($opt->{options}, $opt) || [];
1271 elsif(! scalar(@$data)) {
1272 $ary = [['', errmsg('--no current values--')]];
1274 if($opt->{lookup_exclude}) {
1277 $re = qr/$opt->{lookup_exclude}/;
1281 "Bad lookup pattern m{%s}: %s", $opt->{lookup_exclude}, $@,
1286 $re and @$data = grep "$_->[0]=$_->[1]" !~ /$re/, @$data;
1289 unless($opt->{lookup_merge}) {
1290 unshift @$data, @$ary if $ary;
1295 $existing{$_->[0]}++;
1298 next if $existing{$_->[0]};
1305 ## Some legacy stuff, has to do with default behavior when called from
1306 ## item-accessories or item-options
1309 $adder = $item->{mv_ip} if defined $item->{mv_ip}
1310 and $opt->{item} || ! $opt->{name};
1311 $opt->{name} = $opt->{attribute}
1312 unless $opt->{name};
1313 $opt->{value} = $item->{$opt->{attribute} || $opt->{name}};
1314 $opt->{name} .= $adder if defined $adder;
1315 #::logDebug("tag_accessories: name=$opt->{name} ISHASH");
1318 #::logDebug("display: name=$opt->{name} IS NOT HASH");
1319 $opt->{name} = "mv_order_$opt->{attribute}" unless $opt->{name};
1322 $opt->{price} = get_option_hash($opt->{price_data})
1325 $opt->{name} ||= $opt->{attribute};
1327 if(defined $opt->{value}) {
1330 elsif(defined $item->{$opt->{name}}) {
1331 $opt->{value} = $item->{$opt->{name}};
1333 elsif($opt->{cgi_default} and ! $opt->{override}) {
1334 my $def = $CGI::values{$opt->{name}};
1335 $opt->{value} = $def if defined($def);
1337 elsif($opt->{values_default} and ! $opt->{override}) {
1338 my $def = $::Values->{$opt->{name}};
1339 $opt->{value} = $def if defined($def);
1343 $opt->{textid} = $opt->{id};
1346 $opt->{value} = $opt->{default} if ! defined $opt->{value};
1348 if(length($opt->{blank_default}) and ! length($opt->{value}) ) {
1349 $opt->{value} = $opt->{blank_default};
1352 $opt->{encoded} = encode($opt->{value}, $ESCAPE_CHARS::std);
1353 if($opt->{display_filter}) {
1354 my $newv = Vend::Interpolate::filter_value(
1355 $opt->{display_filter},
1358 $opt->{filtered} = encode($newv, $ESCAPE_CHARS::std);
1360 $opt->{value} =~ s/[/\[/g if $opt->{enable_itl};
1364 $opt->{extra} =~ s{(^|\s+)class=(["'])?[^\s'"]+\2}{$1};
1365 $opt->{extra} =~ s/\s+$//;
1366 $opt->{extra} .= qq{ class="$opt->{class}"};
1369 $opt->{extra} = qq{class="$opt->{class}"};
1373 # Optimization for large lists, we cache the widgets
1374 $Vend::UserWidget ||= Vend::Config::map_widgets();
1375 $Vend::UserWidgetDefault ||= Vend::Config::map_widget_defaults();
1377 my $sub = $Vend::UserWidget->{$type};
1378 if(! $sub and $Global::AccumulateCode) {
1379 $sub = Vend::Config::code_from_file('Widget', $type)
1380 and $Vend::UserWidget->{$type} = $sub;
1383 # Last in case "default" widget is removed
1384 $sub ||= $Vend::UserWidget->{default} || \&template_sub;
1386 if(my $attr = $Vend::UserWidgetDefault->{$type}) {
1387 while (my ($k, $v) = each %$attr) {
1388 next if defined $opt->{$k};
1393 if($opt->{variant}) {
1394 #::logDebug("variant='$opt->{variant}'");
1395 $opt->{subwidget} = $Vend::UserWidget->{$opt->{variant}}
1396 || $Vend::UserWidget->{default};
1399 if(my $c = $opt->{check}) {
1400 $c = "$opt->{name}=$c" unless $c =~ /=/;
1401 HTML::Entities::encode($c);
1402 $opt->{append} .= qq{<input type="hidden" name="mv_individual_profile" value="$c">};
1406 $opt->{extra} ||= '';
1407 $opt->{extra} .= " $opt->{js}";
1408 $opt->{extra} =~ s/^\s+//;
1410 return $sub->($opt, $data);
1415 if(ref($opt) ne 'HASH') {
1416 warn "parse_type: needs passed hash reference";
1420 my %alias = (qw/ datetime date_time /);
1421 my $type = $opt->{type} = lc($opt->{type}) || 'text';
1422 $type = $alias{$type} if $alias{$type};
1423 return $type if $type =~ /^[a-z][a-z0-9]*$/;
1425 if($type =~ /^text/i) {
1427 if ($type =~ /^textarea(?:_(\d+)_(\d+))?(_[a-z]+)?/i) {
1428 my $rows = $1 || $opt->{rows} || 4;
1429 $cols = $2 || $opt->{cols} || 40;
1430 $opt->{type} = 'textarea';
1431 $opt->{rows} = $rows;
1432 $opt->{cols} = $cols;
1434 elsif("\L$type" =~ /^text_?(\d+)$/) {
1436 $opt->{type} = 'text';
1439 $opt->{type} = 'text';
1442 elsif($type =~ /^(date|time)(.*)/i) {
1443 $opt->{type} = lc $1;
1446 $opt->{time} = 1 if $extra =~ /time/i;
1447 $opt->{ampm} = 1 if $extra =~ /ampm/i;
1448 $opt->{blank} = 1 if $extra =~ /blank/i;
1449 $opt->{year_begin} = $1 if $extra =~ s/year_?begin(\d+)//i;
1450 $opt->{year_end} = $1 if $extra =~ s/year_?end(\d+)//i;
1451 ($extra =~ /\(\s*(\s*\d+\s*(,\s*\d+\s*)+)\s*\)/i
1452 and $opt->{minutes} = $1)
1454 ($extra =~ /half/i and $opt->{minutes} = 'half_hourly')
1456 ($extra =~ /hourly/i and $opt->{minutes} = 'hourly')
1458 ($extra =~ /tens/i and $opt->{minutes} = 'tens')
1460 if($extra =~ s/(\d+)-(\d+)//) {
1461 $opt->{start_hour} = $1;
1462 $opt->{end_hour} = $2;
1464 $opt->{time_adjust} = $1
1465 if $extra =~ /([+-]?\d+)/i;
1467 #::logDebug("minutes=$opt->{minutes}");
1469 elsif($type =~ /^hidden_text/i) {
1470 $opt->{type} = 'hiddentext';
1472 elsif($type =~ /^password/i) {
1473 $type =~ /(\d+)/ and $opt->{cols} = $1;
1474 $opt->{type} = 'password';
1476 # Ranging type, for price breaks based on quantity
1477 elsif ($type =~ s/^range:?(.*)//) {
1478 my $select = $1 || 'quantity';
1479 $opt->{type} = 'range';
1481 $opt->{default} = $opt->{item}{$select}
1484 elsif ($type =~ /^(radio|check)/i) {
1485 $opt->{type} = 'box';
1486 if ($type =~ /check/i) {
1487 $opt->{type} = 'checkbox';
1490 $opt->{type} = 'radio';
1493 if ($type =~ /font(?:size)?[\s_]*(-?\d)/i ) {
1494 $opt->{fontsize} = $1;
1497 if($type =~ /nbsp/i) {
1500 elsif ($type =~ /left[\s_]*(\d*)/i ) {
1501 $opt->{breakmod} = $1;
1504 elsif ($type =~ /right[\s_]*(\d*)/i ) {
1505 $opt->{breakmod} = $1;
1509 elsif($type =~ /^combo[ _]*(?:(\d+)(?:[ _]+(\d+))?)?/i) {
1510 $opt->{rows} = $opt->{rows} || $1 || 1;
1511 $opt->{cols} = $opt->{cols} || $2 || 16;
1512 $opt->{type} = 'combo';
1514 elsif($type =~ /^fillin_combo[ _]*(?:(\d+)(?:[ _]+(\d+))?)?/i) {
1515 $opt->{rows} ||= $1;
1516 $opt->{cols} ||= $2;
1517 $opt->{type} = 'combo';
1518 $opt->{textarea} = 1;
1519 $opt->{reverse} = 1;
1520 $opt->{conditional_text} = 1;
1522 elsif($type =~ /^reverse_combo[ _]*(?:(\d+)(?:[ _]+(\d+))?)?/i) {
1523 $opt->{rows} = $opt->{rows} || $1 || 1;
1524 $opt->{cols} = $opt->{cols} || $2 || 16;
1525 $opt->{type} = 'combo';
1526 $opt->{reverse} = 1;
1528 elsif($type =~ /^links_*nbsp/i) {
1530 $opt->{type} = 'links';
1532 elsif($type =~ /^move_*combo[ _]*(?:(\d+)(?:[ _]+(\d+))?)?/i) {
1533 $opt->{rows} = $opt->{rows} || $opt->{height} || $1 || 1;
1534 $opt->{cols} = $opt->{cols} || $opt->{width} || $2 || 16;
1535 $opt->{type} = 'movecombo';
1536 $opt->{replace} = 1 if $type =~ /replace/;
1538 elsif($type =~ /multi/i) {
1539 $opt->{type} = 'select';
1540 $opt->{multiple} = 1;
1541 $type =~ /.*?multiple\s+(.*)/
1542 and $opt->{extra} ||= $1;
1544 elsif($type =~ /^yesno/i) {
1545 $type =~ s/^yesno[_\s]+//;
1546 $opt->{type} = 'yesno';
1548 $opt->{variant} = $type =~ /radio/ ? 'radio' : $type;
1550 elsif($type =~ /^noyes/i) {
1551 $type =~ s/^noyes[_\s]+//;
1552 $opt->{type} = 'noyes';
1554 $opt->{variant} = $type =~ /radio/ ? 'radio' : $type;
1557 return $opt->{type};