1 package SL::Template::Plugin::L;
3 use base qw( Template::Plugin );
5 use List::MoreUtils qw(apply);
6 use List::Util qw(max);
7 use Scalar::Util qw(blessed);
11 { # This will give you an id for identifying html tags and such.
12 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
13 # Do not use these id's to store information across requests.
14 my $_id_sequence = int rand 1e7;
16 return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
20 my %_valueless_attributes = map { $_ => 1 } qw(
21 checked compact declare defer disabled ismap multiple noresize noshade nowrap
27 return $::locale->quote_special_chars('HTML', $string);
31 my $string = "" . shift;
32 $string =~ s/\"/\\\"/g;
37 return (@_ && (ref($_[0]) eq 'HASH')) ? %{ $_[0] } : @_;
41 my ($class, $context, @args) = @_;
49 die 'not an accessor' if @_ > 1;
50 return $_[0]->{CONTEXT};
57 $name =~ s/[^\w_]/_/g;
64 my ($self, @slurp) = @_;
65 my %options = _hashify(@slurp);
68 while (my ($name, $value) = each %options) {
70 next if $_valueless_attributes{$name} && !$value;
71 $value = '' if !defined($value);
72 push @result, $_valueless_attributes{$name} ? _H($name) : _H($name) . '="' . _H($value) . '"';
75 return @result ? ' ' . join(' ', @result) : '';
79 my ($self, $tag, $content, @slurp) = @_;
80 my $attributes = $self->attributes(@slurp);
82 return "<${tag}${attributes}>" unless defined($content);
83 return "<${tag}${attributes}>${content}</${tag}>";
89 my $options_str = shift;
90 my %attributes = _hashify(@_);
92 $attributes{id} ||= $self->name_to_id($name);
93 $options_str = $self->options_for_select($options_str) if ref $options_str;
95 return $self->html_tag('select', $options_str, %attributes, name => $name);
99 my ($self, $name, $content, @slurp) = @_;
100 my %attributes = _hashify(@slurp);
102 $attributes{id} ||= $self->name_to_id($name);
103 $attributes{rows} *= 1; # required by standard
104 $attributes{cols} *= 1; # required by standard
105 $content = $content ? _H($content) : '';
107 return $self->html_tag('textarea', $content, %attributes, name => $name);
111 my ($self, $name, @slurp) = @_;
112 my %attributes = _hashify(@slurp);
114 $attributes{id} ||= $self->name_to_id($name);
115 $attributes{value} = 1 unless defined $attributes{value};
116 my $label = delete $attributes{label};
117 my $checkall = delete $attributes{checkall};
119 if ($attributes{checked}) {
120 $attributes{checked} = 'checked';
122 delete $attributes{checked};
125 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
126 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
127 $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
132 sub radio_button_tag {
135 my %attributes = _hashify(@_);
137 $attributes{value} = 1 unless defined $attributes{value};
138 $attributes{id} ||= $self->name_to_id($name . "_" . $attributes{value});
139 my $label = delete $attributes{label};
141 if ($attributes{checked}) {
142 $attributes{checked} = 'checked';
144 delete $attributes{checked};
147 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
148 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
154 my ($self, $name, $value, @slurp) = @_;
155 my %attributes = _hashify(@slurp);
157 $attributes{id} ||= $self->name_to_id($name);
158 $attributes{type} ||= 'text';
160 return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
164 return shift->input_tag(@_, type => 'hidden');
168 my ($self, $content, @slurp) = @_;
169 return $self->html_tag('div', $content, @slurp);
173 my ($self, $content, @slurp) = @_;
174 return $self->html_tag('ul', $content, @slurp);
178 my ($self, $content, @slurp) = @_;
179 return $self->html_tag('li', $content, @slurp);
183 my ($self, $href, $content, @slurp) = @_;
184 my %params = _hashify(@slurp);
188 return $self->html_tag('a', $content, %params, href => $href);
192 my ($self, $name, $value, @slurp) = @_;
193 my %attributes = _hashify(@slurp);
195 $attributes{onclick} = "if (confirm('" . delete($attributes{confirm}) . "')) return true; else return false;" if $attributes{confirm};
197 return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
201 my ($self, $onclick, $value, @slurp) = @_;
202 my %attributes = _hashify(@slurp);
204 $attributes{id} ||= $self->name_to_id($attributes{name}) if $attributes{name};
205 $attributes{type} ||= 'button';
207 return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
210 sub options_for_select {
212 my $collection = shift;
213 my %options = _hashify(@_);
215 my $value_key = $options{value} || 'id';
216 my $title_key = $options{title} || $value_key;
218 my $value_sub = $options{value_sub};
219 my $title_sub = $options{title_sub};
221 my $value_title_sub = $options{value_title_sub};
223 my %selected = map { ( $_ => 1 ) } @{ ref($options{default}) eq 'ARRAY' ? $options{default} : defined($options{default}) ? [ $options{default} ] : [] };
226 my ($element, $index, $key, $sub) = @_;
227 my $ref = ref $element;
228 return $sub ? $sub->($element)
230 : $ref eq 'ARRAY' ? $element->[$index]
231 : $ref eq 'HASH' ? $element->{$key}
236 push @elements, [ undef, $options{empty_title} || '' ] if $options{with_empty};
237 push @elements, map [
238 $value_title_sub ? @{ $value_title_sub->($_) } : (
239 $access->($_, 0, $value_key, $value_sub),
240 $access->($_, 1, $title_key, $title_sub),
242 ], @{ $collection } if $collection && ref $collection eq 'ARRAY';
245 foreach my $result (@elements) {
246 my %attributes = ( value => $result->[0] );
247 $attributes{selected} = 'selected' if $selected{ defined($result->[0]) ? $result->[0] : '' };
249 $code .= $self->html_tag('option', _H($result->[1]), %attributes);
256 my ($self, $data) = @_;
257 return $self->html_tag('script', $data, type => 'text/javascript');
264 foreach my $file (@_) {
265 $file .= '.css' unless $file =~ m/\.css$/;
266 $file = "css/${file}" unless $file =~ m|/|;
268 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
275 my ($self, $name, $value, @slurp) = @_;
276 my %params = _hashify(@slurp);
277 my $name_e = _H($name);
279 my $datefmt = apply {
283 } $::myconfig{"dateformat"};
285 my $cal_align = delete $params{cal_align} || 'BR';
286 my $onchange = delete $params{onchange};
287 my $str_value = blessed $value ? $value->to_lxoffice : $value;
289 $self->input_tag($name, $str_value,
292 title => _H($::myconfig{dateformat}),
293 onBlur => 'check_right_date_format(this)',
295 onChange => $onchange,
298 ) . ((!$params{no_cal} && !$params{readonly}) ?
299 $self->html_tag('img', undef,
300 src => 'image/calendar.png',
301 alt => $::locale->text('Calendar'),
303 title => _H($::myconfig{dateformat}),
307 "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$cal_align', button: 'trigger$seq' });"
311 sub customer_picker {
312 my ($self, $name, $value, %params) = @_;
313 my $name_e = _H($name);
315 $self->hidden_tag($name, (ref $value && $value->can('id')) ? $value->id : '') .
316 $self->input_tag("$name_e\_name", (ref $value && $value->can('name')) ? $value->name : '', %params) .
317 $self->javascript(<<JS);
318 function autocomplete_customer (selector, column) {
319 \$(function(){ \$(selector).autocomplete({
320 source: function(req, rsp) {
322 url: 'controller.pl?action=Customer/ajax_autocomplete',
327 current: function() { \$('#$name_e').val() },
330 success: function (data){ rsp(data) }
335 select: function(event, ui) {
336 \$('#$name_e').val(ui.item.id);
337 \$('#$name_e\_name').val(ui.item.name);
341 autocomplete_customer('#$name_e\_name');
349 foreach my $file (@_) {
350 $file .= '.js' unless $file =~ m/\.js$/;
351 $file = "js/${file}" unless $file =~ m|/|;
353 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
360 my ($self, $tabs, @slurp) = @_;
361 my %params = _hashify(@slurp);
362 my $id = $params{id} || 'tab_' . _tag_id();
364 $params{selected} *= 1;
366 die 'L.tabbed needs an arrayred of tabs for first argument'
367 unless ref $tabs eq 'ARRAY';
369 my (@header, @blocks);
370 for my $i (0..$#$tabs) {
371 my $tab = $tabs->[$i];
375 my $selected = $params{selected} == $i;
376 my $tab_id = "__tab_id_$i";
377 push @header, $self->li_tag(
378 $self->link('', $tab->{name}, rel => $tab_id),
379 ($selected ? (class => 'selected') : ())
381 push @blocks, $self->div_tag($tab->{data},
382 id => $tab_id, class => 'tabcontent');
385 return '' unless @header;
386 return $self->ul_tag(
387 join('', @header), id => $id, class => 'shadetabs'
390 join('', @blocks), class => 'tabcontentstyle'
393 qq|var $id = new ddtabcontent("$id");$id.setpersist(true);| .
394 qq|$id.setselectedClassTarget("link");$id.init();|
399 my ($self, $name, $src, @slurp) = @_;
400 my %params = _hashify(@slurp);
402 $params{method} ||= 'process';
404 return () if defined $params{if} && !$params{if};
407 if ($params{method} eq 'raw') {
409 } elsif ($params{method} eq 'process') {
410 $data = $self->_context->process($src, %{ $params{args} || {} });
412 die "unknown tag method '$params{method}'";
415 return () unless $data;
417 return +{ name => $name, data => $data };
421 my ($self, $name, $value, @slurp) = @_;
422 my %attributes = _hashify(@slurp);
425 my $min = delete $attributes{min_rows} || 1;
427 if (exists $attributes{cols}) {
428 $cols = delete $attributes{cols};
429 $rows = $::form->numtextrows($value, $cols);
431 $rows = delete $attributes{rows} || 1;
435 ? $self->textarea_tag($name, $value, %attributes, rows => max($rows, $min), ($cols ? (cols => $cols) : ()))
436 : $self->input_tag($name, $value, %attributes, ($cols ? (size => $cols) : ()));
439 sub multiselect2side {
440 my ($self, $id, @slurp) = @_;
441 my %params = _hashify(@slurp);
443 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
444 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
445 $params{moveOptions} = 'false';
447 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
449 <script type="text/javascript">
450 \$().ready(function() {
451 \$('#${id}').multiselect2side({ ${vars} });
459 sub sortable_element {
460 my ($self, $selector, @slurp) = @_;
461 my %params = _hashify(@slurp);
463 my %attributes = ( distance => 5,
464 helper => <<'JAVASCRIPT' );
465 function(event, ui) {
466 ui.children().each(function() {
467 $(this).width($(this).width());
475 if ($params{url} && $params{with}) {
476 my $as = $params{as} || $params{with};
477 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
478 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
480 $stop_event = <<JAVASCRIPT;
481 \$.post('$params{url}', { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
485 if (!$params{dont_recolor}) {
486 $stop_event .= <<JAVASCRIPT;
487 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
488 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
493 $attributes{stop} = <<JAVASCRIPT;
494 function(event, ui) {
501 $params{handle} = '.dragdrop' unless exists $params{handle};
502 $attributes{handle} = "'$params{handle}'" if $params{handle};
504 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
506 my $code = <<JAVASCRIPT;
507 <script type="text/javascript">
509 \$( "${selector}" ).sortable({ ${attr_str} })
517 sub online_help_tag {
518 my ($self, $tag, @slurp) = @_;
519 my %params = _hashify(@slurp);
520 my $cc = $::myconfig{countrycode};
521 my $file = "doc/online/$cc/$tag.html";
522 my $text = $params{text} || $::locale->text('Help');
524 die 'malformed help tag' unless $tag =~ /^[a-zA-Z0-9_]+$/;
525 return unless -f $file;
526 return $self->html_tag('a', $text, href => $file, class => 'jqModal')
531 require Data::Dumper;
532 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
541 SL::Templates::Plugin::L -- Layouting / tag generation
545 Usage from a template:
549 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right' ] ]) %]
551 [% L.select_tag('direction', L.options_for_select([ { direction => 'left', display => 'To the left' },
552 { direction => 'right', display => 'To the right' } ],
553 value => 'direction', title => 'display', default => 'right')) %]
557 A module modeled a bit after Rails' ActionView helpers. Several small
558 functions that create HTML tags from various kinds of data sources.
562 =head2 LOW-LEVEL FUNCTIONS
566 =item C<name_to_id $name>
568 Converts a name to a HTML id by replacing various characters.
570 =item C<attributes %items>
572 Creates a string from all elements in C<%items> suitable for usage as
573 HTML tag attributes. Keys and values are HTML escaped even though keys
574 must not contain non-ASCII characters for browsers to accept them.
576 =item C<html_tag $tag_name, $content_string, %attributes>
578 Creates an opening and closing HTML tag for C<$tag_name> and puts
579 C<$content_string> between the two. If C<$content_string> is undefined
580 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
581 are key/value pairs added to the opening tag.
583 C<$content_string> is not HTML escaped.
587 =head2 HIGH-LEVEL FUNCTIONS
591 =item C<select_tag $name, $options_string, %attributes>
593 Creates a HTML 'select' tag named C<$name> with the contents
594 C<$options_string> and with arbitrary HTML attributes from
595 C<%attributes>. The tag's C<id> defaults to C<name_to_id($name)>.
597 The C<$options_string> is usually created by the
598 L</options_for_select> function. If C<$options_string> is an array
599 reference then it will be passed to L</options_for_select>
602 =item C<input_tag $name, $value, %attributes>
604 Creates a HTML 'input type=text' tag named C<$name> with the value
605 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
606 tag's C<id> defaults to C<name_to_id($name)>.
608 =item C<hidden_tag $name, $value, %attributes>
610 Creates a HTML 'input type=hidden' tag named C<$name> with the value
611 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
612 tag's C<id> defaults to C<name_to_id($name)>.
614 =item C<submit_tag $name, $value, %attributes>
616 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
617 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
618 tag's C<id> defaults to C<name_to_id($name)>.
620 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
621 be added via the C<onclick> handler asking the question given with
622 C<$attributes{confirm}>. If request is only submitted if the user
623 clicks the dialog's ok/yes button.
625 =item C<textarea_tag $name, $value, %attributes>
627 Creates a HTML 'textarea' tag named C<$name> with the content
628 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
629 tag's C<id> defaults to C<name_to_id($name)>.
631 =item C<checkbox_tag $name, %attributes>
633 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
634 HTML attributes from C<%attributes>. The tag's C<id> defaults to
635 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
637 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
638 created with said C<label>. No attribute named C<label> is created in
641 If C<%attributes> contains a key C<checkall> then the value is taken as a
642 JQuery selector and clicking this checkbox will also toggle all checkboxes
643 matching the selector.
645 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
647 Creates a date input field, with an attached javascript that will open a
648 calendar on click. The javascript ist by default anchoered at the bottom right
649 sight. This can be overridden with C<cal_align>, see Calendar documentation for
650 the details, usually you'll want a two letter abbreviation of the alignment.
651 Right + Bottom becomes C<BL>.
653 =item C<radio_button_tag $name, %attributes>
655 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
656 HTML attributes from C<%attributes>. The tag's C<value> defaults to
657 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
659 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
660 created with said C<label>. No attribute named C<label> is created in
663 =item C<javascript_tag $file1, $file2, $file3...>
665 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
666 tag for each file name parameter passed. Each file name will be
667 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
668 doesn't contain a slash.
670 =item C<stylesheet_tag $file1, $file2, $file3...>
672 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
673 for each file name parameter passed. Each file name will be postfixed
674 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
677 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
679 Creates a date input field, with an attached javascript that will open a
680 calendar on click. The javascript ist by default anchoered at the bottom right
681 sight. This can be overridden with C<cal_align>, see Calendar documentation for
682 the details, usually you'll want a two letter abbreviation of the alignment.
683 Right + Bottom becomes C<BL>.
685 =item C<tabbed \@tab, %attributes>
687 Will create a tabbed area. The tabs should be created with the helper function
691 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
692 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
695 An optional attribute is C<selected>, which accepts the ordinal of a tab which
696 should be selected by default.
698 =item C<areainput_tag $name, $content, %PARAMS>
700 Creates a generic input tag or textarea tag, depending on content size. The
701 amount of desired rows must be either given with the C<rows> parameter or can
702 be computed from the value and the C<cols> paramter, Accepted parameters
703 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
705 You can force input by setting rows to 1, and you can force textarea by setting
708 =item C<multiselect2side $id, %params>
710 Creates a JavaScript snippet calling the jQuery function
711 C<multiselect2side> on the select control with the ID C<$id>. The
712 select itself is not created. C<%params> can contain the following
719 The label of the list of available options. Defaults to the
720 translation of 'Available'.
724 The label of the list of selected options. Defaults to the
725 translation of 'Selected'.
729 =item C<sortable_element $selector, %params>
731 Makes the children of the DOM element C<$selector> (a jQuery selector)
732 sortable with the I<jQuery UI Selectable> library. The children can be
733 dragged & dropped around. After dropping an element an URL can be
734 postet to with the element IDs of the sorted children.
736 If this is used then the JavaScript file C<js/jquery-ui.js> must be
737 included manually as well as it isn't loaded via C<$::form-gt;header>.
739 C<%params> can contain the following entries:
745 The URL to POST an AJAX request to after a dragged element has been
746 dropped. The AJAX request's return value is ignored. If given then
747 C<$params{with}> must be given as well.
751 A string that is interpreted as the prefix of the children's ID. Upon
752 POSTing the result each child whose ID starts with C<$params{with}> is
753 considered. The prefix and the following "_" is removed from the
754 ID. The remaining parts of the IDs of those children are posted as a
755 single array parameter. The array parameter's name is either
756 C<$params{as}> or, missing that, C<$params{with}>.
760 Sets the POST parameter name for AJAX request after dropping an
761 element (see C<$params{with}>).
765 An optional jQuery selector specifying which part of the child element
766 is dragable. If the parameter is not given then it defaults to
767 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
768 parameter is set and empty then the whole child element is dragable,
769 and clicks through to underlying elements like inputs or links might
772 =item C<dont_recolor>
774 If trueish then the children will not be recolored. The default is to
775 recolor the children by setting the class C<listrow0> on odd and
776 C<listrow1> on even entries.
782 <script type="text/javascript" src="js/jquery-ui.js"></script>
784 <table id="thing_list">
786 <tr><td>This</td><td>That</td></tr>
789 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
790 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
791 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
795 [% L.sortable_element('#thing_list tbody',
796 url => 'controller.pl?action=SystemThings/reorder',
799 recolor_rows => 1) %]
801 After dropping e.g. the third element at the top of the list a POST
802 request would be made to the C<reorder> action of the C<SystemThings>
803 controller with a single parameter called C<thing_ids> -- an array
804 containing the values C<[ 6, 2, 15 ]>.
808 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
812 =head2 CONVERSION FUNCTIONS
816 =item C<options_for_select \@collection, %options>
818 Creates a string suitable for a HTML 'select' tag consisting of one
819 'E<lt>optionE<gt>' tag for each element in C<\@collection>. The value
820 to use and the title to display are extracted from the elements in
821 C<\@collection>. Each element can be one of four things:
825 =item 1. An array reference with at least two elements. The first element is
826 the value, the second element is its title.
828 =item 2. A scalar. The scalar is both the value and the title.
830 =item 3. A hash reference. In this case C<%options> must contain
831 I<value> and I<title> keys that name the keys in the element to use
832 for the value and title respectively.
834 =item 4. A blessed reference. In this case C<%options> must contain
835 I<value> and I<title> keys that name functions called on the blessed
836 reference whose return values are used as the value and title
841 For cases 3 and 4 C<$options{value}> defaults to C<id> and
842 C<$options{title}> defaults to C<$options{value}>.
844 In addition to pure keys/method you can also provide coderefs as I<value_sub>
845 and/or I<title_sub>. If present, these take precedence over keys or methods,
846 and are called with the element as first argument. It must return the value or
849 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
850 precedence over each individual sub. It will only be called once for each
851 element and must return a list of value and title.
853 If the option C<with_empty> is set then an empty element (value
854 C<undef>) will be used as the first element. The title to display for
855 this element can be set with the option C<empty_title> and defaults to
858 The option C<default> can be either a scalar or an array reference
859 containing the values of the options which should be set to be
862 =item C<tab, description, target, %PARAMS>
864 Creates a tab for C<tabbed>. The description will be used as displayed name.
865 The target should be a block or template that can be processed. C<tab> supports
866 a C<method> parameter, which can override the process method to apply target.
867 C<method => 'raw'> will just include the given text as is. I was too lazy to
868 implement C<include> properly.
870 Also an C<if> attribute is supported, so that tabs can be suppressed based on
871 some occasion. In this case the supplied block won't even get processed, and
872 the resulting tab will get ignored by C<tabbed>:
874 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
878 =head1 MODULE AUTHORS
880 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
882 L<http://linet-services.de>