+ my $self = shift;
+ my $name = shift;
+ my $collection = shift;
+ my %attributes = _hashify(@_);
+
+ $attributes{id} ||= $self->name_to_id($name);
+
+ my $value_key = delete($attributes{value_key}) || 'id';
+ my $title_key = delete($attributes{title_key}) || $value_key;
+ my $default_key = delete($attributes{default_key}) || 'selected';
+
+
+ my $value_title_sub = delete($attributes{value_title_sub});
+
+ my $value_sub = delete($attributes{value_sub});
+ my $title_sub = delete($attributes{title_sub});
+ my $default_sub = delete($attributes{default_sub});
+
+
+ my %selected;
+
+ if ( ref($attributes{default}) eq 'ARRAY' ) {
+
+ foreach my $entry (@{$attributes{default}}) {
+ $selected{$entry} = 1;
+ }
+ } elsif ( defined($attributes{default}) ) {
+ $selected{$attributes{default}} = 1;
+ }
+
+ delete($attributes{default});
+
+
+ my @options;
+
+ if ( delete($attributes{with_empty}) ) {
+ push(@options, [undef, $attributes{empty_title} || '']);
+ }
+
+ my $normalize_entry = sub {
+
+ my ($type, $entry, $sub, $key) = @_;
+
+ if ( $sub ) {
+ return $sub->($entry);
+ }
+
+ my $ref = ref($entry);
+
+ if ( !$ref ) {
+
+ if ( $type eq 'value' || $type eq 'title' ) {
+ return $entry;
+ }
+
+ return 0;
+ }
+
+ if ( $ref eq 'ARRAY' ) {
+
+ if ( $type eq 'value' ) {
+ return $entry->[0];
+ }
+
+ if ( $type eq 'title' ) {
+ return $entry->[1];
+ }
+
+ return $entry->[2];
+ }
+
+ if ( $ref eq 'HASH' ) {
+ return $entry->{$key};
+ }
+
+ if ( $type ne 'default' || $entry->can($key) ) {
+ return $entry->$key;
+ }
+
+ return undef;
+ };
+
+ foreach my $entry ( @{ $collection } ) {
+ my $value;
+ my $title;
+
+ if ( $value_title_sub ) {
+ ($value, $title) = $value_title_sub->($entry);
+ } else {
+
+ $value = $normalize_entry->('value', $entry, $value_sub, $value_key);
+ $title = $normalize_entry->('title', $entry, $title_sub, $title_key);
+ }
+
+ my $default = $normalize_entry->('default', $entry, $default_sub, $default_key);
+
+ push(@options, [$value, $title, $default]);
+ }
+
+ foreach my $entry (@options) {
+ if ( exists($selected{$entry->[0]}) ) {
+ $entry->[2] = 1;
+ }
+ }
+
+ my $code = '';
+
+ foreach my $entry (@options) {
+ my %args = (value => $entry->[0]);
+
+ $args{selected} = $entry->[2];
+
+ $code .= $self->html_tag('option', _H($entry->[1]), %args);
+ }
+
+ $code = $self->html_tag('select', $code, %attributes, name => $name);
+
+ return $code;
+}
+
+sub textarea_tag {
+ my ($self, $name, $content, @slurp) = @_;
+ my %attributes = _hashify(@slurp);
+
+ $attributes{id} ||= $self->name_to_id($name);
+ $attributes{rows} *= 1; # required by standard
+ $attributes{cols} *= 1; # required by standard
+ $content = $content ? _H($content) : '';
+
+ return $self->html_tag('textarea', $content, %attributes, name => $name);
+}
+
+sub checkbox_tag {
+ my ($self, $name, @slurp) = @_;
+ my %attributes = _hashify(@slurp);
+
+ $attributes{id} ||= $self->name_to_id($name);
+ $attributes{value} = 1 unless defined $attributes{value};
+ my $label = delete $attributes{label};
+ my $checkall = delete $attributes{checkall};
+
+ if ($attributes{checked}) {
+ $attributes{checked} = 'checked';
+ } else {
+ delete $attributes{checked};
+ }
+
+ my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
+ $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
+ $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
+
+ return $code;
+}
+
+sub radio_button_tag {
+ my $self = shift;
+ my $name = shift;
+ my %attributes = _hashify(@_);
+
+ $attributes{value} = 1 unless defined $attributes{value};
+ $attributes{id} ||= $self->name_to_id($name . "_" . $attributes{value});
+ my $label = delete $attributes{label};
+
+ if ($attributes{checked}) {
+ $attributes{checked} = 'checked';
+ } else {
+ delete $attributes{checked};
+ }
+
+ my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
+ $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
+
+ return $code;
+}
+
+sub input_tag {
+ my ($self, $name, $value, @slurp) = @_;
+ my %attributes = _hashify(@slurp);
+
+ $attributes{id} ||= $self->name_to_id($name);
+ $attributes{type} ||= 'text';
+
+ return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
+}
+
+sub hidden_tag {
+ return shift->input_tag(@_, type => 'hidden');
+}
+
+sub div_tag {
+ my ($self, $content, @slurp) = @_;
+ return $self->html_tag('div', $content, @slurp);
+}
+
+sub ul_tag {
+ my ($self, $content, @slurp) = @_;
+ return $self->html_tag('ul', $content, @slurp);
+}
+
+sub li_tag {
+ my ($self, $content, @slurp) = @_;
+ return $self->html_tag('li', $content, @slurp);
+}
+
+sub link {
+ my ($self, $href, $content, @slurp) = @_;
+ my %params = _hashify(@slurp);
+
+ $href ||= '#';
+
+ return $self->html_tag('a', $content, %params, href => $href);
+}
+
+sub submit_tag {
+ my ($self, $name, $value, @slurp) = @_;
+ my %attributes = _hashify(@slurp);
+
+ if ( $attributes{confirm} ) {
+ $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
+ }
+
+ return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
+}
+
+sub button_tag {
+ my ($self, $onclick, $value, @slurp) = @_;
+ my %attributes = _hashify(@slurp);
+
+ $attributes{id} ||= $self->name_to_id($attributes{name}) if $attributes{name};
+ $attributes{type} ||= 'button';
+
+ return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
+}
+
+sub yes_no_tag {
+ my ($self, $name, $value) = splice @_, 0, 3;
+ my %attributes = _hashify(@_);
+
+ return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
+}
+
+sub javascript {
+ my ($self, $data) = @_;
+ return $self->html_tag('script', $data, type => 'text/javascript');
+}
+
+sub stylesheet_tag {
+ my $self = shift;
+ my $code = '';
+
+ foreach my $file (@_) {
+ $file .= '.css' unless $file =~ m/\.css$/;
+ $file = "css/${file}" unless $file =~ m|/|;
+
+ $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
+ }
+
+ return $code;
+}
+
+sub date_tag {
+ my ($self, $name, $value, @slurp) = @_;
+ my %params = _hashify(@slurp);
+ my $name_e = _H($name);
+ my $seq = _tag_id();
+ my $datefmt = apply {
+ s/d+/\%d/gi;
+ s/m+/\%m/gi;
+ s/y+/\%Y/gi;
+ } $::myconfig{"dateformat"};
+
+ my $cal_align = delete $params{cal_align} || 'BR';
+ my $onchange = delete $params{onchange};
+ my $str_value = blessed $value ? $value->to_lxoffice : $value;
+
+ $self->input_tag($name, $str_value,
+ id => $name_e,
+ size => 11,
+ title => _H($::myconfig{dateformat}),
+ onBlur => 'check_right_date_format(this)',
+ ($onchange ? (
+ onChange => $onchange,
+ ) : ()),
+ %params,
+ ) . ((!$params{no_cal} && !$params{readonly}) ?
+ $self->html_tag('img', undef,
+ src => 'image/calendar.png',
+ alt => $::locale->text('Calendar'),
+ id => "trigger$seq",
+ title => _H($::myconfig{dateformat}),
+ %params,
+ ) .
+ $self->javascript(
+ "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$cal_align', button: 'trigger$seq' });"
+ ) : '');
+}
+
+sub customer_picker {
+ my ($self, $name, $value, %params) = @_;
+ my $name_e = _H($name);
+
+ $self->hidden_tag($name, (ref $value && $value->can('id')) ? $value->id : '') .
+ $self->input_tag("$name_e\_name", (ref $value && $value->can('name')) ? $value->name : '', %params) .
+ $self->javascript(<<JS);
+function autocomplete_customer (selector, column) {
+ \$(function(){ \$(selector).autocomplete({
+ source: function(req, rsp) {
+ \$.ajax({
+ url: 'controller.pl?action=Customer/ajax_autocomplete',
+ dataType: "json",
+ data: {
+ column: column,
+ term: req.term,
+ current: function() { \$('#$name_e').val() },
+ obsolete: 0,
+ },
+ success: function (data){ rsp(data) }
+ });
+ },
+ limit: 20,
+ delay: 50,
+ select: function(event, ui) {
+ \$('#$name_e').val(ui.item.id);
+ \$('#$name_e\_name').val(ui.item.name);
+ },
+ })});
+}
+autocomplete_customer('#$name_e\_name');
+JS
+}
+
+# simple version with select_tag
+sub vendor_selector {
+ my ($self, $name, $value, %params) = @_;
+
+ my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
+ (ref $value && $value->can('id')) ? $value->id : '';
+
+ return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
+ default => $actual_vendor_id,
+ title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
+ 'with_empty' => 1,
+ %params);
+}
+
+
+# simple version with select_tag
+sub part_selector {
+ my ($self, $name, $value, %params) = @_;
+
+ my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
+ (ref $value && $value->can('id')) ? $value->id : '';
+
+ return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
+ default => $actual_part_id,
+ title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
+ with_empty => 1,
+ %params);
+}
+
+
+sub javascript_tag {
+ my $self = shift;
+ my $code = '';
+
+ foreach my $file (@_) {
+ $file .= '.js' unless $file =~ m/\.js$/;
+ $file = "js/${file}" unless $file =~ m|/|;
+
+ $code .= qq|<script type="text/javascript" src="${file}"></script>|;
+ }
+
+ return $code;
+}
+
+sub tabbed {
+ my ($self, $tabs, @slurp) = @_;
+ my %params = _hashify(@slurp);
+ my $id = $params{id} || 'tab_' . _tag_id();
+
+ $params{selected} *= 1;