More POD-ification
[webmin.git] / ui-lib.pl
1 =head1 ui-lib.pl
2
3 Common functions for generating HTML for Webmin user interface elements.
4 Some example code :
5
6  require '../ui-lib.pl';
7  ui_print_header(undef, 'My Module', '');
8
9  print ui_form_start('save.cgi');
10  print ui_table_start('My form', undef, 2);
11
12  print ui_table_row('Enter your name',
13         ui_textbox('name', undef, 40));
14
15  print ui_table_end();
16  print ui_form_end([ [ undef, 'Save' ] ]);
17
18  ui_print_footer('/', 'Webmin index');
19
20 =cut
21
22 ####################### table generation functions
23
24 =head2 ui_table_start(heading, [tabletags], [cols], [&default-tds], [right-heading])
25
26 Returns HTML for the start of a form block into which labelled inputs can
27 be placed. By default this is implemented as a table with another table inside
28 it, but themes may override this with their own layout.
29
30 The parameters are :
31
32 =item heading - Text to show at the top of the form.
33
34 =item tabletags - HTML attributes to put in the outer <table>, typically something like width=100%.
35
36 =item cols - Desired number of columns for labels and fields. Defaults to 4, but can be 2 for forms with lots of wide inputs.
37
38 =item default-tds - An optional array reference of HTML attributes for the <td> tags in each row of the table.
39
40 =item right-heading - HTML to appear in the heading, aligned to the right.
41
42 =cut
43 sub ui_table_start
44 {
45 return &theme_ui_table_start(@_) if (defined(&theme_ui_table_start));
46 my ($heading, $tabletags, $cols, $tds, $rightheading) = @_;
47 if (defined($main::ui_table_cols)) {
48         # Push on stack, for nested call
49         push(@main::ui_table_cols_stack, $main::ui_table_cols);
50         push(@main::ui_table_pos_stack, $main::ui_table_pos);
51         push(@main::ui_table_default_tds_stack, $main::ui_table_default_tds);
52         }
53 my $colspan = 1;
54 my $rv;
55 $rv .= "<table class='ui_table' border $tabletags>\n";
56 if (defined($heading) || defined($rightheading)) {
57         $rv .= "<tr $tb class='ui_table_head'>";
58         if (defined($heading)) {
59                 $rv .= "<td><b>$heading</b></td>"
60                 }
61         if (defined($rightheading)) {
62                 $rv .= "<td align=right>$rightheading</td>";
63                 $colspan++;
64                 }
65         $rv .= "</tr>\n";
66         }
67 $rv .= "<tr $cb class='ui_table_body'> <td colspan=$colspan>".
68        "<table width=100%>\n";
69 $main::ui_table_cols = $cols || 4;
70 $main::ui_table_pos = 0;
71 $main::ui_table_default_tds = $tds;
72 return $rv;
73 }
74
75 =head2 ui_table_end
76
77 Returns HTML for the end of a block started by ui_table_start.
78
79 =cut
80 sub ui_table_end
81 {
82 return &theme_ui_table_end(@_) if (defined(&theme_ui_table_end));
83 my $rv;
84 if ($main::ui_table_cols == 4 && $main::ui_table_pos) {
85         # Add an empty block to balance the table
86         $rv .= &ui_table_row(" ", " ");
87         }
88 if (@main::ui_table_cols_stack) {
89         $main::ui_table_cols = pop(@main::ui_table_cols_stack);
90         $main::ui_table_pos = pop(@main::ui_table_pos_stack);
91         $main::ui_table_default_tds = pop(@main::ui_table_default_tds_stack);
92         }
93 else {
94         $main::ui_table_cols = undef;
95         $main::ui_table_pos = undef;
96         $main::ui_table_default_tds = undef;
97         }
98 $rv .= "</table></td></tr></table>\n";
99 return $rv;
100 }
101
102 =head2 ui_table_row(label, value, [cols], [&td-tags])
103
104 Returns HTML for a row in a table started by ui_table_start, with a 1-column
105 label and 1+ column value. The parameters are :
106
107 =item label - Label for the input field. If this is undef, no label is displayed.
108
109 =item value - HTML for the input part of the row.
110
111 =item cols - Number of columns the value should take up, defaulting to 1.
112
113 =item td-tags - Array reference of HTML attributes for the <td> tags in this row.
114
115 =cut
116 sub ui_table_row
117 {
118 return &theme_ui_table_row(@_) if (defined(&theme_ui_table_row));
119 my ($label, $value, $cols, $tds) = @_;
120 $cols ||= 1;
121 $tds ||= $main::ui_table_default_tds;
122 my $rv;
123 if ($main::ui_table_pos+$cols+1 > $main::ui_table_cols &&
124     $main::ui_table_pos != 0) {
125         # If the requested number of cols won't fit in the number
126         # remaining, start a new row
127         $rv .= "</tr>\n";
128         $main::ui_table_pos = 0;
129         }
130 $rv .= "<tr class='ui_table_row'>\n"
131         if ($main::ui_table_pos%$main::ui_table_cols == 0);
132 $rv .= "<td valign=top $tds->[0] class='ui_label'><b>$label</b></td>\n"
133         if (defined($label));
134 $rv .= "<td valign=top colspan=$cols $tds->[1] class='ui_value'>$value</td>\n";
135 $main::ui_table_pos += $cols+(defined($label) ? 1 : 0);
136 if ($main::ui_table_pos%$main::ui_table_cols == 0) {
137         $rv .= "</tr>\n";
138         $main::ui_table_pos = 0;
139         }
140 return $rv;
141 }
142
143 =head2 ui_table_hr
144
145 Returns HTML for a row in a block started by ui_table_row, with a horizontal
146 line inside it to separate sections.
147
148 =cut
149 sub ui_table_hr
150 {
151 return &theme_ui_table_hr(@_) if (defined(&theme_ui_table_hr));
152 my $rv;
153 if ($ui_table_pos) {
154         $rv .= "</tr>\n";
155         $ui_table_pos = 0;
156         }
157 $rv .= "<tr class='ui_table_hr'> ".
158        "<td colspan=$main::ui_table_cols><hr></td> </tr>\n";
159 return $rv;
160 }
161
162 =head2 ui_table_span(text)
163
164 Outputs a table row that spans the whole table, and contains the given text.
165
166 =cut
167 sub ui_table_span
168 {
169 my ($text) = @_;
170 return &theme_ui_table_hr(@_) if (defined(&theme_ui_table_hr));
171 my $rv;
172 if ($ui_table_pos) {
173         $rv .= "</tr>\n";
174         $ui_table_pos = 0;
175         }
176 $rv .= "<tr class='ui_table_span'> ".
177        "<td colspan=$main::ui_table_cols>$text</td> </tr>\n";
178 return $rv;
179 }
180
181 =head2 ui_columns_start(&headings, [width-percent], [noborder], [&tdtags], [heading])
182
183 Returns HTML for the start of a multi-column table, with the given headings.
184 The parameters are :
185
186 =item headings - An array reference of headers for the table's columns.
187
188 =item width-percent - Desired width as a percentage, or undef to let the browser decide.
189
190 =item noborder - Set to 1 if the table should not have a border.
191
192 =item tdtags - An optional reference to an array of HTML attributes for the table's <td> tags.
193
194 =item heading - An optional heading to put above the table.
195
196 =cut
197 sub ui_columns_start
198 {
199 return &theme_ui_columns_start(@_) if (defined(&theme_ui_columns_start));
200 my ($heads, $width, $noborder, $tdtags, $title) = @_;
201 my $rv;
202 $rv .= "<table".($noborder ? "" : " border").
203                 (defined($width) ? " width=$width%" : "")." class='ui_columns'>\n";
204 if ($title) {
205         $rv .= "<tr $tb class='ui_columns_heading'>".
206                "<td colspan=".scalar(@$heads)."><b>$title</b></td></tr>\n";
207         }
208 $rv .= "<tr $tb class='ui_columns_heads'>\n";
209 my $i;
210 for($i=0; $i<@$heads; $i++) {
211         $rv .= "<td ".$tdtags->[$i]."><b>".
212                ($heads->[$i] eq "" ? "<br>" : $heads->[$i])."</b></td>\n";
213         }
214 $rv .= "</tr>\n";
215 return $rv;
216 }
217
218 =head2 ui_columns_row(&columns, &tdtags)
219
220 Returns HTML for a row in a multi-column table. The parameters are :
221
222 =item columns - Reference to an array containing the HTML to show in the columns for this row.
223
224 =item tdtags - An optional array reference containing HTML attributes for the row's <td> tags.
225
226 =cut
227 sub ui_columns_row
228 {
229 return &theme_ui_columns_row(@_) if (defined(&theme_ui_columns_row));
230 my ($cols, $tdtags) = @_;
231 my $rv;
232 $rv .= "<tr $cb class='ui_columns_row'>\n";
233 my $i;
234 for($i=0; $i<@$cols; $i++) {
235         $rv .= "<td ".$tdtags->[$i].">".
236                ($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i])."</td>\n";
237         }
238 $rv .= "</tr>\n";
239 return $rv;
240 }
241
242 =head2 ui_columns_header(&columns, &tdtags)
243
244 Returns HTML for a row in a multi-column table, styled as a header. Parameters
245 are the same as ui_columns_row.
246
247 =cut
248 sub ui_columns_header
249 {
250 return &theme_ui_columns_header(@_) if (defined(&theme_ui_columns_header));
251 my ($cols, $tdtags) = @_;
252 my $rv;
253 $rv .= "<tr $tb class='ui_columns_header'>\n";
254 my $i;
255 for($i=0; $i<@$cols; $i++) {
256         $rv .= "<td ".$tdtags->[$i]."><b>".
257                ($cols->[$i] eq "" ? "<br>" : $cols->[$i])."</b></td>\n";
258         }
259 $rv .= "</tr>\n";
260 return $rv;
261 }
262
263 =head2 ui_checked_columns_row(&columns, &tdtags, checkname, checkvalue, [checked?], [disabled])
264
265 Returns HTML for a row in a multi-column table, in which the first column 
266 contains a checkbox. The parameters are :
267
268 =item columns - Reference to an array containing the HTML to show in the columns for this row.
269
270 =item tdtags - An optional array reference containing HTML attributes for the row's <td> tags.
271
272 =item checkname - Name for the checkbox input. Should be the same for all rows.
273
274 =item checkvalue - Value for this checkbox input.
275
276 =item checked - Set to 1 if it should be checked by default.
277
278 =item disabled - Set to 1 if the checkbox should be disabled and thus un-clickable.
279
280 =cut
281 sub ui_checked_columns_row
282 {
283 return &theme_ui_checked_columns_row(@_) if (defined(&theme_ui_checked_columns_row));
284 my ($cols, $tdtags, $checkname, $checkvalue, $checked, $disabled) = @_;
285 my $rv;
286 $rv .= "<tr $cb class='ui_checked_columns'>\n";
287 $rv .= "<td class='ui_checked_checkbox' ".$tdtags->[0].">".
288        &ui_checkbox($checkname, $checkvalue, undef, $checked, undef, $disabled).
289        "</td>\n";
290 my $i;
291 for($i=0; $i<@$cols; $i++) {
292         $rv .= "<td ".$tdtags->[$i+1].">";
293         if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
294                 $rv .= "<label for=\"".
295                         &quote_escape("${checkname}_${checkvalue}")."\">";
296                 }
297         $rv .= ($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i]);
298         if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
299                 $rv .= "</label>";
300                 }
301         $rv .= "</td>\n";
302         }
303 $rv .= "</tr>\n";
304 return $rv;
305 }
306
307 =head2 ui_radio_columns_row(&columns, &tdtags, checkname, checkvalue, [checked], [disabled])
308
309 Returns HTML for a row in a multi-column table, in which the first
310 column is a radio button. The parameters are :
311
312 =item columns - Reference to an array containing the HTML to show in the columns for this row.
313
314 =item tdtags - An optional array reference containing HTML attributes for the row's <td> tags.
315
316 =item checkname - Name for the radio button input. Should be the same for all rows.
317
318 =item checkvalue - Value for this radio button option.
319
320 =item checked - Set to 1 if it should be checked by default.
321
322 =item disabled - Set to 1 if the radio button should be disabled and thus un-clickable.
323
324 =cut
325 sub ui_radio_columns_row
326 {
327 return &theme_ui_radio_columns_row(@_) if (defined(&theme_ui_radio_columns_row));
328 my ($cols, $tdtags, $checkname, $checkvalue, $checked, $dis) = @_;
329 my $rv;
330 $rv .= "<tr $cb class='ui_radio_columns'>\n";
331 $rv .= "<td class='ui_radio_radio' ".$tdtags->[0].">".
332     &ui_oneradio($checkname, $checkvalue, "", $checked, undef, $dis)."</td>\n";
333 my $i;
334 for($i=0; $i<@$cols; $i++) {
335         $rv .= "<td ".$tdtags->[$i+1].">";
336         if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
337                 $rv .= "<label for=\"".
338                         &quote_escape("${checkname}_${checkvalue}")."\">";
339                 }
340         $rv .= ($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i]);
341         if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
342                 $rv .= "</label>";
343                 }
344         $rv .= "</td>\n";
345         }
346 $rv .= "</tr>\n";
347 return $rv;
348 }
349
350 =head2 ui_columns_end
351
352 Returns HTML to end a table started by ui_columns_start.
353
354 =cut
355 sub ui_columns_end
356 {
357 return &theme_ui_columns_end(@_) if (defined(&theme_ui_columns_end));
358 return "</table>\n";
359 }
360
361 =head2 ui_columns_table(&headings, width-percent, &data, &types, no-sort, title, empty-msg)
362
363 Returns HTML for a complete table, typically generated internally by
364 ui_columns_start, ui_columns_row and ui_columns_end. The parameters are :
365
366 =item headings - An array ref of heading HTML.
367
368 =item width-percent - Preferred total width
369
370 =item data - A 2x2 array ref of table contents. Each can either be a simple string, or a hash ref like :
371
372   { 'type' => 'group', 'desc' => 'Some section title' }
373   { 'type' => 'string', 'value' => 'Foo', 'colums' => 3,
374     'nowrap' => 1 }
375   { 'type' => 'checkbox', 'name' => 'd', 'value' => 'foo',
376     'label' => 'Yes', 'checked' => 1, 'disabled' => 1 }
377   { 'type' => 'radio', 'name' => 'd', 'value' => 'foo', ... }
378
379 =item types - An array ref of data types, such as 'string', 'number', 'bytes' or 'date'
380
381 =item no-sort - Set to 1 to disable sorting by theme.
382
383 =item title - Text to appear above the table.
384
385 =item empty-msg - Message to display if no data.
386
387 =cut
388 sub ui_columns_table
389 {
390 return &theme_ui_columns_table(@_) if (defined(&theme_ui_columns_table));
391 my ($heads, $width, $data, $types, $nosort, $title, $emptymsg) = @_;
392 my $rv;
393
394 # Just show empty message if no data
395 if ($emptymsg && !@$data) {
396         $rv .= &ui_subheading($title) if ($title);
397         $rv .= "<span class='ui_emptymsg'><b>$emptymsg</b></span><p>\n";
398         return $rv;
399         }
400
401 # Are there any checkboxes in each column? If so, make those columns narrow
402 my @tds = map { "valign=top" } @$heads;
403 my $maxwidth = 0;
404 foreach my $r (@$data) {
405         my $cc = 0;
406         foreach my $c (@$r) {
407                 if (ref($c) &&
408                     ($c->{'type'} eq 'checkbox' || $c->{'type'} eq 'radio')) {
409                         $tds[$cc] .= " width=5" if ($tds[$cc] !~ /width=/);
410                         }
411                 $cc++;
412                 }
413         $maxwidth = $cc if ($cc > $maxwidth);
414         }
415 $rv .= &ui_columns_start($heads, $width, 0, \@tds, $title);
416
417 # Add the data rows
418 foreach my $r (@$data) {
419         my $c0;
420         if (ref($r->[0]) && ($r->[0]->{'type'} eq 'checkbox' ||
421                              $r->[0]->{'type'} eq 'radio')) {
422                 # First column is special
423                 $c0 = $r->[0];
424                 $r = [ @$r[1..(@$r-1)] ];
425                 }
426         # Turn data into HTML
427         my @rtds = @tds;
428         my @cols;
429         my $cn = 0;
430         $cn++ if ($c0);
431         foreach my $c (@$r) {
432                 if (!ref($c)) {
433                         # Plain old string
434                         push(@cols, $c);
435                         }
436                 elsif ($c->{'type'} eq 'checkbox') {
437                         # Checkbox in non-first column
438                         push(@cols, &ui_checkbox($c->{'name'}, $c->{'value'},
439                                                  $c->{'label'}, $c->{'checked'},
440                                                  undef, $c->{'disabled'}));
441                         }
442                 elsif ($c->{'type'} eq 'radio') {
443                         # Radio button in non-first column
444                         push(@cols, &ui_oneradio($c->{'name'}, $c->{'value'},
445                                                  $c->{'label'}, $c->{'checked'},
446                                                  undef, $c->{'disabled'}));
447                         }
448                 elsif ($c->{'type'} eq 'group') {
449                         # Header row that spans whole table
450                         $rv .= &ui_columns_header([ $c->{'desc'} ],
451                                                   [ "colspan=$width" ]);
452                         next;
453                         }
454                 elsif ($c->{'type'} eq 'string') {
455                         # A string, which might be special
456                         push(@cols, $c->{'value'});
457                         if ($c->{'columns'} > 1) {
458                                 splice(@rtds, $cn, $c->{'columns'},
459                                        "colspan=".$c->{'columns'});
460                                 }
461                         if ($c->{'nowrap'}) {
462                                 $rtds[$cn] .= " nowrap";
463                                 }
464                         }
465                 $cn++;
466                 }
467         # Add the row
468         if (!$c0) {
469                 $rv .= &ui_columns_row(\@cols, \@rtds);
470                 }
471         elsif ($c0->{'type'} eq 'checkbox') {
472                 $rv .= &ui_checked_columns_row(\@cols, \@rtds, $c0->{'name'},
473                                                $c0->{'value'}, $c0->{'checked'},
474                                                $c0->{'disabled'});
475                 }
476         elsif ($c0->{'type'} eq 'radio') {
477                 $rv .= &ui_radio_columns_row(\@cols, \@rtds, $c0->{'name'},
478                                              $c0->{'value'}, $c0->{'checked'},
479                                              $c0->{'disabled'});
480                 }
481         }
482
483 $rv .= &ui_columns_end();
484 return $rv;
485 }
486
487 =head2 ui_form_columns_table(cgi, &buttons, select-all, &otherlinks, &hiddens, &headings, width-percent, &data, &types, no-sort, title, empty-msg)
488
489 Similar to ui_columns_table, but wrapped in a form. Parameters are :
490
491 =item cgi - URL to submit the form to.
492
493 =item buttons - An array ref of buttons at the end of the form, similar to that taken by ui_form_end.
494
495 =item select-all - If set to 1, include select all / invert links.
496
497 =item otherslinks - An array ref of other links to put at the top of the table, each of which is a 3-element hash ref of url, text and alignment (left or right).
498
499 =item hiddens - An array ref of hidden fields, each of which is a 2-element array ref containing the name and value.
500
501 All other parameters are the same as ui_columns_table.
502
503 =cut
504 sub ui_form_columns_table
505 {
506 return &theme_ui_form_columns_table(@_)
507         if (defined(&theme_ui_form_columns_table));
508 my ($cgi, $buttons, $selectall, $others, $hiddens,
509        $heads, $width, $data, $types, $nosort, $title, $emptymsg) = @_;
510 my $rv;
511
512 # Build links
513 my @leftlinks = map { "<a href='$_->[0]'>$_->[1]</a>" }
514                        grep { $_->[2] ne 'right' } @$others;
515 my @rightlinks = map { "<a href='$_->[0]'>$_->[1]</a>" }
516                        grep { $_->[2] eq 'right' } @$others;
517 my $links;
518
519 # Add select links
520 if (@$data) {
521         if ($selectall) {
522                 my $cbname;
523                 foreach my $r (@$data) {
524                         foreach my $c (@$r) {
525                                 if (ref($c) && $c->{'type'} eq 'checkbox') {
526                                         $cbname = $c->{'name'};
527                                         last;
528                                         }
529                                 }
530                         }
531                 if ($cbname) {
532                         unshift(@leftlinks, &select_all_link($cbname),
533                                             &select_invert_link($cbname));
534                         }
535                 }
536         }
537
538 # Turn to HTML
539 if (@rightlinks) {
540         $links = &ui_grid_table([ &ui_links_row(\@leftlinks),
541                                   &ui_links_row(\@rightlinks) ], 2, 100,
542                                 [ undef, "align=right" ]);
543         }
544 elsif (@leftlinks) {
545         $links = &ui_links_row(\@leftlinks);
546         }
547
548 # Start the form, if we need one
549 if (@$data) {
550         $rv .= &ui_form_start($cgi, "post");
551         foreach my $h (@$hiddens) {
552                 $rv .= &ui_hidden(@$h);
553                 }
554         $rv .= $links;
555         }
556
557 # Add the table
558 $rv .= &ui_columns_table($heads, $width, $data, $types, $nosort, $title,
559                          $emptymsg);
560
561 # Add form end
562 $rv .= $links;
563 if (@$data) {
564         $rv .= &ui_form_end($buttons);
565         }
566
567 return $rv;
568 }
569
570 ####################### form generation functions
571
572 =head2 ui_form_start(script, method, [target], [tags])
573
574 Returns HTML for the start of a a form that submits to some script. The
575 parameters are :
576
577 =item script - CGI script to submit to, like save.cgi.
578
579 =item method - HTTP method, which must be one of 'get', 'post' or 'form-data'. If form-data is used, the target CGI must call ReadParseMime to parse parameters.
580
581 =item target - Optional target window or frame for the form.
582
583 =item tags - Additional HTML attributes for the form tag.
584
585 =cut
586 sub ui_form_start
587 {
588 return &theme_ui_form_start(@_) if (defined(&theme_ui_form_start));
589 my ($script, $method, $target, $tags) = @_;
590 my $rv;
591 $rv .= "<form class='ui_form' action='".&html_escape($script)."' ".
592         ($method eq "post" ? "method=post" :
593          $method eq "form-data" ?
594                 "method=post enctype=multipart/form-data" :
595                 "method=get").
596         ($target ? " target=$target" : "").
597         " ".$tags.
598        ">\n";
599 return $rv;
600 }
601
602 =head2 ui_form_end([&buttons], [width])
603
604 Returns HTML for the end of a form, optionally with a row of submit buttons.
605 These are specified by the buttons parameter, which is an array reference
606 of array refs, with the following elements :
607
608 =item HTML value for the submit input for the button, or undef for none.
609
610 =item Text to appear on the button.
611
612 =item HTML or other inputs to appear after the button.
613
614 =item Set to 1 if the button should be disabled.
615
616 =item Additional HTML attributes to appear inside the button's input tag.
617
618 =cut
619 sub ui_form_end
620 {
621 return &theme_ui_form_end(@_) if (defined(&theme_ui_form_end));
622 my ($buttons, $width) = @_;
623 my $rv;
624 if ($buttons && @$buttons) {
625         $rv .= "<table class='ui_form_end_buttons' ".($width ? " width=$width" : "")."><tr>\n";
626         my $b;
627         foreach $b (@$buttons) {
628                 if (ref($b)) {
629                         $rv .= "<td".(!$width ? "" :
630                                       $b eq $buttons->[0] ? " align=left" :
631                                       $b eq $buttons->[@$buttons-1] ?
632                                         " align=right" : " align=center").">".
633                                &ui_submit($b->[1], $b->[0], $b->[3], $b->[4]).
634                                ($b->[2] ? " ".$b->[2] : "")."</td>\n";
635                         }
636                 elsif ($b) {
637                         $rv .= "<td>$b</td>\n";
638                         }
639                 else {
640                         $rv .= "<td>&nbsp;&nbsp;</td>\n";
641                         }
642                 }
643         $rv .= "</tr></table>\n";
644         }
645 $rv .= "</form>\n";
646 return $rv;
647 }
648
649 =head2 ui_textbox(name, value, size, [disabled?], [maxlength], [tags])
650
651 Returns HTML for a text input box. The parameters are :
652
653 =item name - Name for this input.
654
655 =item value - Initial contents for the text box.
656
657 =item size - Desired width in characters.
658
659 =item disabled - Set to 1 if this text box should be disabled by default.
660
661 =item maxlength - Maximum length of the string the user is allowed to input.
662
663 =item tags - Additional HTML attributes for the <input> tag.
664
665 =cut
666 sub ui_textbox
667 {
668 return &theme_ui_textbox(@_) if (defined(&theme_ui_textbox));
669 my ($name, $value, $size, $dis, $max, $tags) = @_;
670 $size = &ui_max_text_width($size);
671 return "<input class='ui_textbox' name=\"".&quote_escape($name)."\" ".
672        "value=\"".&quote_escape($value)."\" ".
673        "size=$size ".($dis ? "disabled=true" : "").
674        ($max ? " maxlength=$max" : "").
675        " ".$tags.
676        ">";
677 }
678
679 =head2 ui_filebox(name, value, size, [disabled?], [maxlength], [tags], [dir-only])
680
681 Returns HTML for a text box for choosing a file. Parameters are the same
682 as ui_textbox, except for the extra dir-only option which limits the chooser
683 to directories.
684
685 =cut
686 sub ui_filebox
687 {
688 return &theme_ui_filebox(@_) if (defined(&theme_ui_filebox));
689 my ($name, $value, $size, $dis, $max, $tags, $dironly) = @_;
690 return &ui_textbox($name, $value, $size, $dis, $max, $tags)."&nbsp;".
691        &file_chooser_button($name, $dironly);
692 }
693
694 =head2 ui_bytesbox(name, bytes, [size], [disabled?])
695
696 Returns HTML for entering a number of bytes, but with friendly kB/MB/GB
697 options. May truncate values to 2 decimal points! The parameters are :
698
699 =item name - Name for this input.
700
701 =item bytes - Initial number of bytes to show.
702
703 =item size - Desired width of the text box part.
704
705 =item disabled - Set to 1 if this text box should be disabled by default.
706
707 =cut
708 sub ui_bytesbox
709 {
710 my ($name, $bytes, $size, $dis) = @_;
711 my $units = 1;
712 if ($bytes >= 10*1024*1024*1024*1024) {
713         $units = 1024*1024*1024*1024;
714         }
715 elsif ($bytes >= 10*1024*1024*1024) {
716         $units = 1024*1024*1024;
717         }
718 elsif ($bytes >= 10*1024*1024) {
719         $units = 1024*1024;
720         }
721 elsif ($bytes >= 10*1024) {
722         $units = 1024;
723         }
724 else {
725         $units = 1;
726         }
727 if ($bytes ne "") {
728         $bytes = sprintf("%.2f", ($bytes*1.0)/$units);
729         $bytes =~ s/\.00$//;
730         }
731 $size = &ui_max_text_width($size || 8);
732 return &ui_textbox($name, $bytes, $size, $dis)." ".
733        &ui_select($name."_units", $units,
734                  [ [ 1, "bytes" ],
735                    [ 1024, "kB" ],
736                    [ 1024*1024, "MB" ],
737                    [ 1024*1024*1024, "GB" ],
738                    [ 1024*1024*1024*1024, "TB" ] ], undef, undef, undef, $dis);
739 }
740
741 =head2 ui_upload(name, size, [disabled?], [tags])
742
743 Returns HTML for a file upload input, for use in a form with the form-data
744 method. The parameters are :
745
746 =item name - Name for this input.
747
748 =item size - Desired width in characters.
749
750 =item disabled - Set to 1 if this text box should be disabled by default.
751
752 =item tags - Additional HTML attributes for the <input> tag.
753
754 =cut
755 sub ui_upload
756 {
757 return &theme_ui_upload(@_) if (defined(&theme_ui_upload));
758 my ($name, $size, $dis, $tags) = @_;
759 $size = &ui_max_text_width($size);
760 return "<input class='ui_upload' type=file name=\"".&quote_escape($name)."\" ".
761        "size=$size ".
762        ($dis ? "disabled=true" : "").
763        ($tags ? " ".$tags : "").">";
764 }
765
766 =head2 ui_password(name, value, size, [disabled?], [maxlength])
767
768 Returns HTML for a password text input. Parameters are the same as ui_textbox,
769 and behaviour is identical except that the user's input is not visible.
770
771 =cut
772 sub ui_password
773 {
774 return &theme_ui_password(@_) if (defined(&theme_ui_password));
775 my ($name, $value, $size, $dis, $max) = @_;
776 $size = &ui_max_text_width($size);
777 return "<input class='ui_password' ".
778        "type=password name=\"".&quote_escape($name)."\" ".
779        "value=\"".&quote_escape($value)."\" ".
780        "size=$size ".($dis ? "disabled=true" : "").
781        ($max ? " maxlength=$max" : "").
782        ">";
783 }
784
785 =head2 ui_hidden(name, value)
786
787 Returns HTML for a hidden field with the given name and value.
788
789 =cut
790 sub ui_hidden
791 {
792 return &theme_ui_hidden(@_) if (defined(&theme_ui_hidden));
793 my ($name, $value) = @_;
794 return "<input class='ui_hidden' type=hidden ".
795        "name=\"".&quote_escape($name)."\" ".
796        "value=\"".&quote_escape($value)."\">\n";
797 }
798
799 =head2 ui_select(name, value|&values, &options, [size], [multiple], [add-if-missing], [disabled?], [javascript])
800
801 Returns HTML for a drop-down menu or multiple selection list. The parameters
802 are :
803
804 =item name - Name for this input.
805
806 =item value - Either a single initial value, or an array reference of values if this is a multi-select list.
807
808 =item options - An array reference of possible options. Each element can either be a scalar, or a two-element array ref containing a submitted value and displayed text.
809
810 =item size - Desired vertical size in rows, which defaults to 1. For multi-select lists, this must be set to something larger.
811
812 =item multiple - Set to 1 for a multi-select list, 0 for single.
813
814 =item add-if-missing - If set to 1, any value that is not in the list of options will be automatically added (and selected).
815
816 =item disabled - Set to 1 to disable this input.
817
818 =item javascript - Additional HTML attributes for the <select> input.
819
820 =cut
821 sub ui_select
822 {
823 return &theme_ui_select(@_) if (defined(&theme_ui_select));
824 my ($name, $value, $opts, $size, $multiple, $missing, $dis, $js) = @_;
825 my $rv;
826 $rv .= "<select class='ui_select' name=\"".&quote_escape($name)."\"".
827        ($size ? " size=$size" : "").
828        ($multiple ? " multiple" : "").
829        ($dis ? " disabled=true" : "")." ".$js.">\n";
830 my ($o, %opt, $s);
831 my %sel = ref($value) ? ( map { $_, 1 } @$value ) : ( $value, 1 );
832 foreach $o (@$opts) {
833         $o = [ $o ] if (!ref($o));
834         $rv .= "<option value=\"".&quote_escape($o->[0])."\"".
835                ($sel{$o->[0]} ? " selected" : "")." ".$o->[2].">".
836                ($o->[1] || $o->[0])."\n";
837         $opt{$o->[0]}++;
838         }
839 foreach $s (keys %sel) {
840         if (!$opt{$s} && $missing) {
841                 $rv .= "<option value=\"".&quote_escape($s)."\"".
842                        "selected>".($s eq "" ? "&nbsp;" : $s)."\n";
843                 }
844         }
845 $rv .= "</select>\n";
846 return $rv;
847 }
848
849 =head2 ui_multi_select(name, &values, &options, size, [add-if-missing], [disabled?], [options-title, values-title], [width])
850
851 Returns HTML for selecting many of many from a list. By default, this is
852 implemented using two <select> lists and Javascript buttons to move elements
853 between them. The resulting input value is \n separated.
854
855 Parameters are :
856
857 =item name - HTML name for this input.
858
859 =item values - An array reference of two-element array refs, containing the submitted values and descriptions of items that are selected by default.
860
861 =item options - An array reference of two-element array refs, containing the submitted values and descriptions of items that the user can select from.
862
863 =item size - Vertical size in rows.
864
865 =item add-if-missing - If set to 1, any entries that are in values but not in options will be added automatically.
866
867 =item disabled - Set to 1 to disable this input by default.
868
869 =item options-title - Optional text to appear above the list of options.
870
871 =item values-title - Optional text to appear above the list of selected values.
872
873 =item width - Optional width of the two lists in pixels.
874
875 =cut
876 sub ui_multi_select
877 {
878 return &theme_ui_multi_select(@_) if (defined(&theme_ui_multi_select));
879 my ($name, $values, $opts, $size, $missing, $dis,
880        $opts_title, $vals_title, $width) = @_;
881 my $rv;
882 my %already = map { $_->[0], $_ } @$values;
883 my $leftover = [ grep { !$already{$_->[0]} } @$opts ];
884 if ($missing) {
885         my %optsalready = map { $_->[0], $_ } @$opts;
886         push(@$opts, grep { !$optsalready{$_->[0]} } @$values);
887         }
888 if (!defined($width)) {
889         $width = "200";
890         }
891 my $wstyle = $width ? "style='width:$width'" : "";
892
893 if (!$main::ui_multi_select_donejs++) {
894         $rv .= &ui_multi_select_javascript();
895         }
896 $rv .= "<table cellpadding=0 cellspacing=0 class='ui_multi_select'>";
897 if (defined($opts_title)) {
898         $rv .= "<tr class='ui_multi_select_heads'> ".
899                "<td><b>$opts_title</b></td> ".
900                "<td></td> <td><b>$vals_title</b></td> </tr>";
901         }
902 $rv .= "<tr class='ui_multi_select_row'>";
903 $rv .= "<td>".&ui_select($name."_opts", [ ], $leftover,
904                          $size, 0, 0, $dis, $wstyle)."</td>\n";
905 $rv .= "<td>".&ui_button("->", undef, $dis,
906                  "onClick='multi_select_move(\"$name\", form, 1)'")."<br>".
907               &ui_button("<-", undef, $dis,
908                  "onClick='multi_select_move(\"$name\", form, 0)'")."</td>\n";
909 $rv .= "<td>".&ui_select($name."_vals", [ ], $values,
910                          $size, 0, 0, $dis, $wstyle)."</td>\n";
911 $rv .= "</tr></table>\n";
912 $rv .= &ui_hidden($name, join("\n", map { $_->[0] } @$values));
913 return $rv;
914 }
915
916 =head2 ui_multi_select_javascript
917
918 Returns <script> section for left/right select boxes. For internal use only.
919
920 =cut
921 sub ui_multi_select_javascript
922 {
923 return &theme_ui_multiselect_javascript()
924         if (defined(&theme_ui_multiselect_javascript));
925 return <<EOF;
926 <script>
927 // Move an element from the options list to the values list, or vice-versa
928 function multi_select_move(name, f, dir)
929 {
930 var opts = f.elements[name+"_opts"];
931 var vals = f.elements[name+"_vals"];
932 var opts_idx = opts.selectedIndex;
933 var vals_idx = vals.selectedIndex;
934 if (dir == 1 && opts_idx >= 0) {
935         // Moving from options to selected list
936         var o = opts.options[opts_idx];
937         vals.options[vals.options.length] = new Option(o.text, o.value);
938         opts.remove(opts_idx);
939         }
940 else if (dir == 0 && vals_idx >= 0) {
941         // Moving the other way
942         var o = vals.options[vals_idx];
943         opts.options[opts.options.length] = new Option(o.text, o.value);
944         vals.remove(vals_idx);
945         }
946 // Fill in hidden field
947 var hid = f.elements[name];
948 if (hid) {
949         var hv = new Array();
950         for(var i=0; i<vals.options.length; i++) {
951                 hv.push(vals.options[i].value);
952                 }
953         hid.value = hv.join("\\n");
954         }
955 }
956 </script>
957 EOF
958 }
959
960 =head2 ui_radio(name, value, &options, [disabled?])
961
962 Returns HTML for a series of radio buttons, of which one can be selected. The
963 parameters are :
964
965 =item name - HTML name for the radio buttons.
966
967 =item value - Value of the button that is selected by default.
968
969 =item options - Array ref of radio button options, each of which is an array ref containing the submitted value and description for each button.
970
971 =item disabled - Set to 1 to disable all radio buttons by default.
972
973 =cut
974 sub ui_radio
975 {
976 return &theme_ui_radio(@_) if (defined(&theme_ui_radio));
977 my ($name, $value, $opts, $dis) = @_;
978 my $rv;
979 my $o;
980 foreach $o (@$opts) {
981         my $id = &quote_escape($name."_".$o->[0]);
982         my $label = $o->[1] || $o->[0];
983         my $after;
984         if ($label =~ /^(.*?)((<a\s+href|<input|<select|<textarea)[\000-\377]*)$/i) {
985                 $label = $1;
986                 $after = $2;
987                 }
988         $rv .= "<input class='ui_radio' type=radio ".
989                "name=\"".&quote_escape($name)."\" ".
990                "value=\"".&quote_escape($o->[0])."\"".
991                ($o->[0] eq $value ? " checked" : "").
992                ($dis ? " disabled=true" : "").
993                " id=\"$id\"".
994                " $o->[2]> <label for=\"$id\">".
995                $label."</label>".$after."\n";
996         }
997 return $rv;
998 }
999
1000 =head2 ui_yesno_radio(name, value, [yes], [no], [disabled?])
1001
1002 Like ui_radio, but always displays just two inputs (yes and no). The parameters
1003 are :
1004
1005 =item name - HTML name of the inputs.
1006
1007 =item value - Option selected by default, typically 1 or 0.
1008
1009 =item yes - The value for the yes option, defaulting to 1.
1010
1011 =item no - The value for the no option, defaulting to 0.
1012
1013 =item disabled - Set to 1 to disable all radio buttons by default.
1014
1015 =cut
1016 sub ui_yesno_radio
1017 {
1018 my ($name, $value, $yes, $no, $dis) = @_;
1019 return &theme_ui_yesno_radio(@_) if (defined(&theme_ui_yesno_radio));
1020 $yes = 1 if (!defined($yes));
1021 $no = 0 if (!defined($no));
1022 $value = int($value);
1023 return &ui_radio($name, $value, [ [ $yes, $text{'yes'} ],
1024                                   [ $no, $text{'no'} ] ], $dis);
1025 }
1026
1027 =head2 ui_checkbox(name, value, label, selected?, [tags], [disabled?])
1028
1029 Returns HTML for a single checkbox. Parameters are :
1030
1031 =item name - HTML name of the checkbox.
1032
1033 =item value - Value that will be submitted if it is checked.
1034
1035 =item label - Text to appear next to the checkbox.
1036
1037 =item selected - Set to 1 for it to be checked by default.
1038
1039 =item tags - Additional HTML attributes for the <input> tag.
1040
1041 =item disabled - Set to 1 to disable the checkbox by default.
1042
1043 =cut
1044 sub ui_checkbox
1045 {
1046 return &theme_ui_checkbox(@_) if (defined(&theme_ui_checkbox));
1047 my ($name, $value, $label, $sel, $tags, $dis) = @_;
1048 my $after;
1049 if ($label =~ /^([^<]*)(<[\000-\377]*)$/) {
1050         $label = $1;
1051         $after = $2;
1052         }
1053 return "<input class='ui_checkbox' type=checkbox ".
1054        "name=\"".&quote_escape($name)."\" ".
1055        "value=\"".&quote_escape($value)."\" ".
1056        ($sel ? " checked" : "").($dis ? " disabled=true" : "").
1057        " id=\"".&quote_escape("${name}_${value}")."\"".
1058        " $tags> ".
1059        ($label eq "" ? $after :
1060          "<label for=\"".&quote_escape("${name}_${value}").
1061          "\">$label</label>$after")."\n";
1062 }
1063
1064 =head2 ui_oneradio(name, value, label, selected?, [tags], [disabled?])
1065
1066 Returns HTML for a single radio button. The parameters are :
1067
1068 =item name - HTML name of the radio button.
1069
1070 =item value - Value that will be submitted if it is selected.
1071
1072 =item label - Text to appear next to the button.
1073
1074 =item selected - Set to 1 for it to be selected by default.
1075
1076 =item tags - Additional HTML attributes for the <input> tag.
1077
1078 =item disabled - Set to 1 to disable the radio button by default.
1079
1080 =cut
1081 sub ui_oneradio
1082 {
1083 return &theme_ui_oneradio(@_) if (defined(&theme_ui_oneradio));
1084 my ($name, $value, $label, $sel, $tags, $dis) = @_;
1085 my $id = &quote_escape("${name}_${value}");
1086 my $after;
1087 if ($label =~ /^([^<]*)(<[\000-\377]*)$/) {
1088         $label = $1;
1089         $after = $2;
1090         }
1091 return "<input class='ui_radio' type=radio name=\"".&quote_escape($name)."\" ".
1092        "value=\"".&quote_escape($value)."\" ".
1093        ($sel ? " checked" : "").($dis ? " disabled=true" : "").
1094        " id=\"$id\"".
1095        " $tags> <label for=\"$id\">$label</label>$after\n";
1096 }
1097
1098 =head2 ui_textarea(name, value, rows, cols, [wrap], [disabled?], [tags])
1099
1100 Returns HTML for a multi-line text input. The function parameters are :
1101
1102 =item name - Name for this HTML <textarea>.
1103
1104 =item value - Default value. Multiple lines must be separated by \n.
1105
1106 =item rows - Number of rows, in lines.
1107
1108 =item cols - Number of columns, in characters.
1109
1110 =item wrap - Wrapping mode. Can be one of soft, hard or off.
1111
1112 =item disabled - Set to 1 to disable this text area by default.
1113
1114 =item tags - Additional HTML attributes for the <textarea> tag.
1115
1116 =cut
1117 sub ui_textarea
1118 {
1119 return &theme_ui_textarea(@_) if (defined(&theme_ui_textarea));
1120 my ($name, $value, $rows, $cols, $wrap, $dis, $tags) = @_;
1121 $cols = &ui_max_text_width($cols, 1);
1122 return "<textarea class='ui_textarea' name=\"".&quote_escape($name)."\" ".
1123        "rows=$rows cols=$cols".($wrap ? " wrap=$wrap" : "").
1124        ($dis ? " disabled=true" : "").
1125        ($tags ? " $tags" : "").">".
1126        &html_escape($value).
1127        "</textarea>";
1128 }
1129
1130 =head2 ui_user_textbox(name, value, [form], [disabled?], [tags])
1131
1132 Returns HTML for an input for selecting a Unix user. Parameters are the
1133 same as ui_textbox.
1134
1135 =cut
1136 sub ui_user_textbox
1137 {
1138 return &theme_ui_user_textbox(@_) if (defined(&theme_ui_user_textbox));
1139 return &ui_textbox($_[0], $_[1], 13, $_[3], undef, $_[4])." ".
1140        &user_chooser_button($_[0], 0, $_[2]);
1141 }
1142
1143 =head2 ui_group_textbox(name, value, [form], [disabled?], [tags])
1144
1145 Returns HTML for an input for selecting a Unix group. Parameters are the
1146 same as ui_textbox.
1147
1148 =cut
1149 sub ui_group_textbox
1150 {
1151 return &theme_ui_group_textbox(@_) if (defined(&theme_ui_group_textbox));
1152 return &ui_textbox($_[0], $_[1], 13, $_[3], undef, $_[4])." ".
1153        &group_chooser_button($_[0], 0, $_[2]);
1154 }
1155
1156 =head2 ui_opt_textbox(name, value, size, option1, [option2], [disabled?], [&extra-fields], [max])
1157
1158 Returns HTML for a text field that is optional, implemented by default as
1159 a field with radio buttons next to it. The parameters are :
1160
1161 =item name - HTML name for the text box. The radio buttons will have the same name, but with _def appended.
1162
1163 =item value - Initial value, or undef if you want the default radio button selected initially.
1164
1165 =item size - Width of the text box in characters.
1166
1167 =item option1 - Text for the radio button for selecting that no input is being given, such as 'Default'.
1168
1169 =item option2 - Text for the radio button for selecting that you will provide input.
1170
1171 =item disabled - Set to 1 to disable this input by default.
1172
1173 =item extra-fields - An optional array ref of field names that should be disabled by Javascript when this field is disabled.
1174
1175 =item max - Optional maximum allowed input length, in characters.
1176
1177 =cut
1178 sub ui_opt_textbox
1179 {
1180 return &theme_ui_opt_textbox(@_) if (defined(&theme_ui_opt_textbox));
1181 my ($name, $value, $size, $opt1, $opt2, $dis, $extra, $max) = @_;
1182 my $dis1 = &js_disable_inputs([ $name, @$extra ], [ ]);
1183 my $dis2 = &js_disable_inputs([ ], [ $name, @$extra ]);
1184 my $rv;
1185 $size = &ui_max_text_width($size);
1186 $rv .= &ui_radio($name."_def", $value eq '' ? 1 : 0,
1187                  [ [ 1, $opt1, "onClick='$dis1'" ],
1188                    [ 0, $opt2 || " ", "onClick='$dis2'" ] ], $dis)."\n";
1189 $rv .= "<input class='ui_opt_textbox' name=\"".&quote_escape($name)."\" ".
1190        "size=$size value=\"".&quote_escape($value)."\" ".
1191        ($value eq "" || $dis ? "disabled=true" : "").
1192        ($max ? " maxlength=$max" : "").">\n";
1193 return $rv;
1194 }
1195
1196 =head2 ui_submit(label, [name], [disabled?], [tags])
1197
1198 Returns HTML for a form submit button. Parameters are :
1199
1200 =item label - Text to appear on the button.
1201
1202 =item name - Optional HTML name for the button. Useful if the CGI it submits to needs to know which of several buttons was clicked.
1203
1204 =item disabled - Set to 1 if this button should be disabled by default.
1205
1206 =item tags - Additional HTML attributes for the <input> tag.
1207
1208 =cut
1209 sub ui_submit
1210 {
1211 return &theme_ui_submit(@_) if (defined(&theme_ui_submit));
1212 my ($label, $name, $dis, $tags) = @_;
1213 return "<input class='ui_submit' type=submit".
1214        ($name ne '' ? " name=\"".&quote_escape($name)."\"" : "").
1215        " value=\"".&quote_escape($label)."\"".
1216        ($dis ? " disabled=true" : "").
1217        ($tags ? " ".$tags : "").">\n";
1218                         
1219 }
1220
1221 =head2 ui_reset(label, [disabled?])
1222
1223 Returns HTML for a form reset button, which clears all fields when clicked.
1224 Parameters are :
1225
1226 =item label - Text to appear on the button.
1227
1228 =item disabled - Set to 1 if this button should be disabled by default.
1229
1230 =cut
1231 sub ui_reset
1232 {
1233 return &theme_ui_reset(@_) if (defined(&theme_ui_reset));
1234 my ($label, $dis) = @_;
1235 return "<input type=reset value=\"".&quote_escape($label)."\"".
1236        ($dis ? " disabled=true" : "").">\n";
1237                         
1238 }
1239
1240 =head2 ui_button(label, [name], [disabled?], [tags])
1241
1242 Returns HTML for a form button, which doesn't do anything when clicked unless
1243 you add some Javascript to it. The parameters are :
1244
1245 =item label - Text to appear on the button.
1246
1247 =item name - HTML name for this input.
1248
1249 =item disabled - Set to 1 if this button should be disabled by default.
1250
1251 =item tags - Additional HTML attributes for the <input> tag, typically Javascript inside an onClick attribute.
1252
1253 =cut
1254 sub ui_button
1255 {
1256 return &theme_ui_button(@_) if (defined(&theme_ui_button));
1257 my ($label, $name, $dis, $tags) = @_;
1258 return "<input type=button".
1259        ($name ne '' ? " name=\"".&quote_escape($name)."\"" : "").
1260        " value=\"".&quote_escape($label)."\"".
1261        ($dis ? " disabled=true" : "").
1262        ($tags ? " ".$tags : "").">\n";
1263 }
1264
1265 =head2 ui_date_input(day, month, year, day-name, month-name, year-name, [disabled?])
1266
1267 Returns HTML for a date-selection field, with day, month and year inputs.
1268 The parameters are :
1269
1270 =item day - Initial day of the month.
1271
1272 =item month - Initial month of the year, indexed from 1.
1273
1274 =item year - Initial year, four-digit.
1275
1276 =item day-name - Name of the day input field.
1277
1278 =item month-name - Name of the month select field.
1279
1280 =item year-name - Name of the year input field.
1281
1282 =item disabled - Set to 1 to disable all fields by default.
1283
1284 =cut
1285 sub ui_date_input
1286 {
1287 my ($day, $month, $year, $dayname, $monthname, $yearname, $dis) = @_;
1288 my $rv;
1289 $rv .= "<span class='ui_data'>";
1290 $rv .= &ui_textbox($dayname, $day, 3, $dis);
1291 $rv .= "/";
1292 $rv .= &ui_select($monthname, $month,
1293                   [ map { [ $_, $text{"smonth_$_"} ] } (1 .. 12) ],
1294                   1, 0, 0, $dis);
1295 $rv .= "/";
1296 $rv .= &ui_textbox($yearname, $year, 5, $dis);
1297 $rv .= "</span>";
1298 return $rv;
1299 }
1300
1301 =head2 ui_buttons_start
1302
1303 Returns HTML for the start of a block of action buttoms with descriptions, as
1304 generated by ui_buttons_row. Some example code :
1305
1306   print ui_buttons_start();
1307   print ui_buttons_row('start.cgi', 'Start server',
1308                        'Click this button to start the server process');
1309   print ui_buttons_row('stop.cgi', 'Stop server',
1310                        'Click this button to stop the server process');
1311   print ui_buttons_end();
1312
1313 =cut
1314 sub ui_buttons_start
1315 {
1316 return &theme_ui_buttons_start(@_) if (defined(&theme_ui_buttons_start));
1317 return "<table width=100% class='ui_buttons_table'>\n";
1318 }
1319
1320 =head2 ui_buttons_end
1321
1322 Returns HTML for the end of a block started by ui_buttons_start.
1323
1324 =cut
1325 sub ui_buttons_end
1326 {
1327 return &theme_ui_buttons_end(@_) if (defined(&theme_ui_buttons_end));
1328 return "</table>\n";
1329 }
1330
1331 =head2 ui_buttons_row(script, button-label, description, [hiddens], [after-submit], [before-submit]) 
1332
1333 Returns HTML for a button with a description next to it, and perhaps other
1334 inputs. The parameters are :
1335
1336 =item script - CGI script that this button submits to, like start.cgi.
1337
1338 =item button-label - Text to appear on the button.
1339
1340 =item description - Text to appear next to the button, describing in more detail what it does.
1341
1342 =item hiddens - HTML for hidden fields to include in the form this function generates.
1343
1344 =item after-submit - HTML for text or inputs to appear after the submit button.
1345
1346 =item before-submit - HTML for text or inputs to appear before the submit button.
1347
1348 =cut
1349 sub ui_buttons_row
1350 {
1351 return &theme_ui_buttons_row(@_) if (defined(&theme_ui_buttons_row));
1352 my ($script, $label, $desc, $hiddens, $after, $before) = @_;
1353 return "<form action=$script class='ui_buttons_form'>\n".
1354        $hiddens.
1355        "<tr class='ui_buttons_row'> ".
1356        "<td nowrap width=20% valign=top class=ui_buttons_label>".
1357        ($before ? $before." " : "").
1358        &ui_submit($label).($after ? " ".$after : "")."</td>\n".
1359        "<td valign=top width=80% valign=top class=ui_buttons_value>".
1360        $desc."</td> </tr>\n".
1361        "</form>\n";
1362 }
1363
1364 =head2 ui_buttons_hr([title])
1365
1366 Returns HTML for a separator row, for use inside a ui_buttons_start block.
1367
1368 =cut
1369 sub ui_buttons_hr
1370 {
1371 my ($title) = @_;
1372 return &theme_ui_buttons_hr(@_) if (defined(&theme_ui_buttons_hr));
1373 if ($title) {
1374         return "<tr class='ui_buttons_hr'> <td colspan=2><table cellpadding=0 cellspacing=0 width=100%><tr> <td width=50%><hr></td> <td nowrap>$title</td> <td width=50%><hr></td> </tr></table></td> </tr>\n";
1375         }
1376 else {
1377         return "<tr class='ui_buttons_hr'> <td colspan=2><hr></td> </tr>\n";
1378         }
1379 }
1380
1381 ####################### header and footer functions
1382
1383 =head2 ui_post_header([subtext])
1384
1385 Returns HTML to appear directly after a standard header() call. This is never
1386 called directly - instead, ui_print_header calls it. But it can be overridden
1387 by themes.
1388
1389 =cut
1390 sub ui_post_header
1391 {
1392 return &theme_ui_post_header(@_) if (defined(&theme_ui_post_header));
1393 my ($text) = @_;
1394 my $rv;
1395 $rv .= "<center class='ui_post_header'><font size=+1>$text</font></center>\n" if (defined($text));
1396 if (!$tconfig{'nohr'} && !$tconfig{'notophr'}) {
1397         $rv .= "<hr id='post_header_hr'>\n";
1398         }
1399 return $rv;
1400 }
1401
1402 =head2 ui_pre_footer
1403
1404 Returns HTML to appear directly before a standard footer() call. This is never
1405 called directly - instead, ui_print_footer calls it. But it can be overridden
1406 by themes.
1407
1408 =cut
1409 sub ui_pre_footer
1410 {
1411 return &theme_ui_pre_footer(@_) if (defined(&theme_ui_pre_footer));
1412 my $rv;
1413 if (!$tconfig{'nohr'} && !$tconfig{'nobottomhr'}) {
1414         $rv .= "<hr id='pre_footer_hr'>\n";
1415         }
1416 return $rv;
1417 }
1418
1419 =head2 ui_print_header(subtext, args...)
1420
1421 Print HTML for a header with the post-header line. The args are the same
1422 as those passed to header(), defined in web-lib-funcs.pl. The exception is
1423 the additional subtext parameter, which is for optional HTML to display
1424 just below the header.
1425
1426 =cut
1427 sub ui_print_header
1428 {
1429 &load_theme_library();
1430 return &theme_ui_print_header(@_) if (defined(&theme_ui_print_header));
1431 my ($text, @args) = @_;
1432 &header(@args);
1433 print &ui_post_header($text);
1434 }
1435
1436 =head2 ui_print_unbuffered_header(subtext, args...)
1437
1438 Like ui_print_header, but ensures that output for this page is not buffered
1439 or contained in a table. This should be called by scripts that are producing
1440 output while performing some long-running process.
1441
1442 =cut
1443 sub ui_print_unbuffered_header
1444 {
1445 &load_theme_library();
1446 return &theme_ui_print_unbuffered_header(@_) if (defined(&theme_ui_print_unbuffered_header));
1447 $| = 1;
1448 $theme_no_table = 1;
1449 &ui_print_header(@_);
1450 }
1451
1452 =head2 ui_print_footer(args...)
1453
1454 Print HTML for a footer with the pre-footer line. Args are the same as those
1455 passed to footer().
1456
1457 =cut
1458 sub ui_print_footer
1459 {
1460 return &theme_ui_print_footer(@_) if (defined(&theme_ui_print_footer));
1461 my @args = @_;
1462 print &ui_pre_footer();
1463 &footer(@args);
1464 }
1465
1466 =head2 ui_config_link(text, &subs)
1467
1468 Returns HTML for a module config link. The first non-null sub will be
1469 replaced with the appropriate URL for the module's config page.
1470
1471 =cut
1472 sub ui_config_link
1473 {
1474 return &theme_ui_config_link(@_) if (defined(&theme_ui_config_link));
1475 my ($text, $subs) = @_;
1476 my @subs = map { $_ || "../config.cgi?$module_name" }
1477                   ($subs ? @$subs : ( undef ));
1478 return "<p>".&text($text, @subs)."<p>\n";
1479 }
1480
1481 =head2 ui_print_endpage(text)
1482
1483 Prints HTML for an error message followed by a page footer with a link to
1484 /, then exits. Good for main page error messages.
1485
1486 =cut
1487 sub ui_print_endpage
1488 {
1489 return &theme_ui_print_endpage(@_) if (defined(&theme_ui_print_endpage));
1490 my ($text) = @_;
1491 print $text,"<p class='ui_footer'>\n";
1492 print "</p>\n";
1493 &ui_print_footer("/", $text{'index'});
1494 exit;
1495 }
1496
1497 =head2 ui_subheading(text, ...)
1498
1499 Returns HTML for a section heading whose message is the given text strings.
1500
1501 =cut
1502 sub ui_subheading
1503 {
1504 return &theme_ui_subheading(@_) if (defined(&theme_ui_subheading));
1505 return "<h3 class='ui_subheading'>".join("", @_)."</h3>\n";
1506 }
1507
1508 =head2 ui_links_row(&links)
1509
1510 Returns HTML for a row of links, like select all / invert selection / add..
1511 Each element of the links array ref should be an HTML fragment like :
1512
1513   <a href='user_form.cgi'>Create new user</a>
1514
1515 =cut
1516 sub ui_links_row
1517 {
1518 return &theme_ui_links_row(@_) if (defined(&theme_ui_links_row));
1519 my ($links) = @_;
1520 return @$links ? join("\n|\n", @$links)."<br>\n"
1521                : "";
1522 }
1523
1524 ########################### collapsible section / tab functions
1525
1526 =head2 ui_hidden_javascript
1527
1528 Returns <script> and <style> sections for hiding functions and CSS. For
1529 internal use only.
1530
1531 =cut
1532 sub ui_hidden_javascript
1533 {
1534 return &theme_ui_hidden_javascript(@_)
1535         if (defined(&theme_ui_hidden_javascript));
1536 my $rv;
1537 my $imgdir = "$gconfig{'webprefix'}/images";
1538 my ($jscb, $jstb) = ($cb, $tb);
1539 $jscb =~ s/'/\\'/g;
1540 $jstb =~ s/'/\\'/g;
1541
1542 return <<EOF;
1543 <style>
1544 .opener_shown {display:inline}
1545 .opener_hidden {display:none}
1546 </style>
1547 <script>
1548 // Open or close a hidden section
1549 function hidden_opener(divid, openerid)
1550 {
1551 var divobj = document.getElementById(divid);
1552 var openerobj = document.getElementById(openerid);
1553 if (divobj.className == 'opener_shown') {
1554   divobj.className = 'opener_hidden';
1555   openerobj.innerHTML = '<img border=0 src=$imgdir/closed.gif>';
1556   }
1557 else {
1558   divobj.className = 'opener_shown';
1559   openerobj.innerHTML = '<img border=0 src=$imgdir/open.gif>';
1560   }
1561 }
1562
1563 // Show a tab
1564 function select_tab(name, tabname, form)
1565 {
1566 var tabnames = document[name+'_tabnames'];
1567 var tabtitles = document[name+'_tabtitles'];
1568 for(var i=0; i<tabnames.length; i++) {
1569   var tabobj = document.getElementById('tab_'+tabnames[i]);
1570   var divobj = document.getElementById('div_'+tabnames[i]);
1571   var title = tabtitles[i];
1572   if (tabnames[i] == tabname) {
1573     // Selected table
1574     tabobj.innerHTML = '<table cellpadding=0 cellspacing=0><tr>'+
1575                        '<td valign=top $jscb>'+
1576                        '<img src=$imgdir/lc2.gif alt=""></td>'+
1577                        '<td $jscb nowrap>'+
1578                        '&nbsp;<b>'+title+'</b>&nbsp;</td>'+
1579                        '<td valign=top $jscb>'+
1580                        '<img src=$imgdir/rc2.gif alt=""></td>'+
1581                        '</tr></table>';
1582     divobj.className = 'opener_shown';
1583     }
1584   else {
1585     // Non-selected tab
1586     tabobj.innerHTML = '<table cellpadding=0 cellspacing=0><tr>'+
1587                        '<td valign=top $jstb>'+
1588                        '<img src=$imgdir/lc1.gif alt=""></td>'+
1589                        '<td $jstb nowrap>'+
1590                        '&nbsp;<a href=\\'\\' onClick=\\'return select_tab("'+
1591                        name+'", "'+tabnames[i]+'")\\'>'+title+'</a>&nbsp;</td>'+
1592                        '<td valign=top $jstb>'+
1593                        '<img src=$imgdir/rc1.gif alt=""></td>'+
1594                        '</tr></table>';
1595     divobj.className = 'opener_hidden';
1596     }
1597   }
1598 if (document.forms[0] && document.forms[0][name]) {
1599   document.forms[0][name].value = tabname;
1600   }
1601 return false;
1602 }
1603 </script>
1604 EOF
1605 }
1606
1607 =head2 ui_hidden_start(title, name, status, thisurl)
1608
1609 Returns HTML for the start of a collapsible hidden section, such as for
1610 advanced options. When clicked on, the section header will expand to display
1611 whatever is between this function and ui_hidden_end. The parameters are :
1612
1613 =item title - Text for the start of this hidden section.
1614
1615 =item name - A unique name for this section.
1616
1617 =item status - 1 if it should be initially open, 0 if not.
1618
1619 =item thisurl - URL of the current page. This is used by themes on devices that don't support Javascript to implement the opening and closing.
1620
1621 =cut
1622 sub ui_hidden_start
1623 {
1624 return &theme_ui_hidden_start(@_) if (defined(&theme_ui_hidden_start));
1625 my ($title, $name, $status, $url) = @_;
1626 my $rv;
1627 if (!$main::ui_hidden_start_donejs++) {
1628         $rv .= &ui_hidden_javascript();
1629         }
1630 my $divid = "hiddendiv_$name";
1631 my $openerid = "hiddenopener_$name";
1632 my $defimg = $status ? "open.gif" : "closed.gif";
1633 my $defclass = $status ? 'opener_shown' : 'opener_hidden';
1634 $rv .= "<a href=\"javascript:hidden_opener('$divid', '$openerid')\" id='$openerid'><img border=0 src='$gconfig{'webprefix'}/images/$defimg' alt='*'></a>\n";
1635 $rv .= "<a href=\"javascript:hidden_opener('$divid', '$openerid')\">$title</a><br>\n";
1636 $rv .= "<div class='$defclass' id='$divid'>\n";
1637 return $rv;
1638 }
1639
1640 =head2 ui_hidden_end(name)
1641
1642 Returns HTML for the end of a hidden section, started by ui_hidden_start.
1643
1644 =cut
1645 sub ui_hidden_end
1646 {
1647 return &theme_ui_hidden_end(@_) if (defined(&theme_ui_hidden_end));
1648 my ($name) = @_;
1649 return "</div>\n";
1650 }
1651
1652 =head2 ui_hidden_table_row_start(title, name, status, thisurl)
1653
1654 Similar to ui_hidden_start, but for use within a table started with
1655 ui_table_start. I recommend against using this where possible, as it can
1656 be difficult for some themes to implement.
1657
1658 =cut
1659 sub ui_hidden_table_row_start
1660 {
1661 return &theme_ui_hidden_table_row_start(@_)
1662         if (defined(&theme_ui_hidden_table_row_start));
1663 my ($title, $name, $status, $url) = @_;
1664 my ($rv, $rrv);
1665 if (!$main::ui_hidden_start_donejs++) {
1666         $rv .= &ui_hidden_javascript();
1667         }
1668 my $divid = "hiddendiv_$name";
1669 my $openerid = "hiddenopener_$name";
1670 my $defimg = $status ? "open.gif" : "closed.gif";
1671 my $defclass = $status ? 'opener_shown' : 'opener_hidden';
1672 $rrv .= "<a href=\"javascript:hidden_opener('$divid', '$openerid')\" id='$openerid'><img border=0 src='$gconfig{'webprefix'}/images/$defimg'></a>\n";
1673 $rrv .= "<a href=\"javascript:hidden_opener('$divid', '$openerid')\">$title</a><br>\n";
1674 $rv .= &ui_table_row(undef, $rrv, $main::ui_table_cols);
1675 $rv .= "</table>\n";
1676 $rv .= "<div class='$defclass' id='$divid'>\n";
1677 $rv .= "<table width=100%>\n";
1678 return $rv;
1679 }
1680
1681 =head2 ui_hidden_table_row_end(name)
1682
1683 Returns HTML to end a block started by ui_hidden_table_start.
1684
1685 =cut
1686 sub ui_hidden_table_row_end
1687 {
1688 return &theme_ui_hidden_table_row_end(@_)
1689         if (defined(&theme_ui_hidden_table_row_end));
1690 my ($name) = @_;
1691 return "</table></div><table width=100%>\n";
1692 }
1693
1694 =head2 ui_hidden_table_start(heading, [tabletags], [cols], name, status, [&default-tds], [rightheading])
1695
1696 Returns HTML for the start of a form block into which labelled inputs can
1697 be placed, which is collapsible by clicking on the header. Basically the same
1698 as ui_table_start, and must contain HTML generated by ui_table_row.
1699
1700 The parameters are :
1701
1702 =item heading - Text to show at the top of the form.
1703
1704 =item tabletags - HTML attributes to put in the outer <table>, typically something like width=100%.
1705
1706 =item cols - Desired number of columns for labels and fields. Defaults to 4, but can be 2 for forms with lots of wide inputs.
1707
1708 =item name - A unique name for this table.
1709
1710 =item status - Set to 1 if initially open, 0 if initially closed.
1711
1712 =item default-tds - An optional array reference of HTML attributes for the <td> tags in each row of the table.
1713
1714 =item right-heading - HTML to appear in the heading, aligned to the right.
1715
1716 =cut
1717 sub ui_hidden_table_start
1718 {
1719 return &theme_ui_hidden_table_start(@_)
1720         if (defined(&theme_ui_hidden_table_start));
1721 my ($heading, $tabletags, $cols, $name, $status, $tds, $rightheading) = @_;
1722 my $rv;
1723 if (!$main::ui_hidden_start_donejs++) {
1724         $rv .= &ui_hidden_javascript();
1725         }
1726 my $divid = "hiddendiv_$name";
1727 my $openerid = "hiddenopener_$name";
1728 my $defimg = $status ? "open.gif" : "closed.gif";
1729 my $defclass = $status ? 'opener_shown' : 'opener_hidden';
1730 my $text = defined($tconfig{'cs_text'}) ? $tconfig{'cs_text'} : 
1731               defined($gconfig{'cs_text'}) ? $gconfig{'cs_text'} : "000000";
1732 $rv .= "<table class='ui_table' border $tabletags>\n";
1733 my $colspan = 1;
1734 if (defined($heading) || defined($rightheading)) {
1735         $rv .= "<tr $tb> <td>";
1736         if (defined($heading)) {
1737                 $rv .= "<a href=\"javascript:hidden_opener('$divid', '$openerid')\" id='$openerid'><img border=0 src='$gconfig{'webprefix'}/images/$defimg'></a> <a href=\"javascript:hidden_opener('$divid', '$openerid')\"><b><font color=#$text>$heading</font></b></a></td>";
1738                 }
1739         if (defined($rightheading)) {
1740                 $rv .= "<td align=right>$rightheading</td>";
1741                 $colspan++;
1742                 }
1743         $rv .= "</td> </tr>\n";
1744         }
1745 $rv .= "<tr $cb> <td colspan=$colspan><div class='$defclass' id='$divid'><table width=100%>\n";
1746 $main::ui_table_cols = $cols || 4;
1747 $main::ui_table_pos = 0;
1748 $main::ui_table_default_tds = $tds;
1749 return $rv;
1750 }
1751
1752 =head2 ui_hidden_table_end(name)
1753
1754 Returns HTML for the end of a form block with hiding, as started by
1755 ui_hidden_table_start.
1756
1757 =cut
1758 sub ui_hidden_table_end
1759 {
1760 my ($name) = @_;
1761 return &theme_ui_hidden_table_end(@_) if (defined(&theme_ui_hidden_table_end));
1762 return "</table></div></td></tr></table>\n";
1763 }
1764
1765 =head2 ui_tabs_start(&tabs, name, selected, show-border)
1766
1767 Returns a row of tabs from which one can be selected, displaying HTML
1768 associated with that tab. The parameters are :
1769
1770 =item tabs - An array reference of array refs, each of which contains the value and user-visible text for a tab.
1771
1772 =item name - Name of the HTML field into which the selected tab will be placed.
1773
1774 =item selected - Value for the tab selected by default.
1775
1776 =item show-border - Set to 1 if there should be a border around the contents of the tabs.
1777
1778 Example code :
1779
1780   @tabs = ( [ 'list', 'List services' ],
1781             [ 'install', 'Install new service' ] );
1782   print ui_tabs_start(\@tabs, 'mode', 'list');
1783
1784   print ui_tabs_start_tab('mode', 'list');
1785   generate_service_list();
1786   print ui_tabs_end_tab('mode', 'list');
1787
1788   print ui_tabs_start_tab('mode', 'install');
1789   generate_install_form();
1790   print ui_tabs_end_tab('mode', 'install);
1791
1792   print ui_tabs_end();
1793
1794 =cut
1795 sub ui_tabs_start
1796 {
1797 return &theme_ui_tabs_start(@_) if (defined(&theme_ui_tabs_start));
1798 my ($tabs, $name, $sel, $border) = @_;
1799 my $rv;
1800 if (!$main::ui_hidden_start_donejs++) {
1801         $rv .= &ui_hidden_javascript();
1802         }
1803
1804 # Build list of tab titles and names
1805 my $tabnames = "[".join(",", map { "\"".&html_escape($_->[0])."\"" } @$tabs)."]";
1806 my $tabtitles = "[".join(",", map { "\"".&html_escape($_->[1])."\"" } @$tabs)."]";
1807 $rv .= "<script>\n";
1808 $rv .= "document.${name}_tabnames = $tabnames;\n";
1809 $rv .= "document.${name}_tabtitles = $tabtitles;\n";
1810 $rv .= "</script>\n";
1811
1812 # Output the tabs
1813 my $imgdir = "$gconfig{'webprefix'}/images";
1814 $rv .= &ui_hidden($name, $sel)."\n";
1815 $rv .= "<table border=0 cellpadding=0 cellspacing=0 class='ui_tabs'>\n";
1816 $rv .= "<tr><td bgcolor=#ffffff colspan=".(scalar(@$tabs)*2+1).">";
1817 if ($ENV{'HTTP_USER_AGENT'} !~ /msie/i) {
1818         # For some reason, the 1-pixel space above the tabs appears huge on IE!
1819         $rv .= "<img src=$imgdir/1x1.gif>";
1820         }
1821 $rv .= "</td></tr>\n";
1822 $rv .= "<tr>\n";
1823 $rv .= "<td bgcolor=#ffffff width=1><img src=$imgdir/1x1.gif></td>\n";
1824 foreach my $t (@$tabs) {
1825         if ($t ne $tabs[0]) {
1826                 # Spacer
1827                 $rv .= "<td width=2 bgcolor=#ffffff class='ui_tab_spacer'>".
1828                        "<img src=$imgdir/1x1.gif></td>\n";
1829                 }
1830         my $tabid = "tab_".$t->[0];
1831         $rv .= "<td id=${tabid} class='ui_tab'>";
1832         $rv .= "<table cellpadding=0 cellspacing=0 border=0><tr>";
1833         if ($t->[0] eq $sel) {
1834                 # Selected tab
1835                 $rv .= "<td valign=top $cb class='selectedTabLeft'>".
1836                        "<img src=$imgdir/lc2.gif alt=\"\"></td>";
1837                 $rv .= "<td $cb nowrap class='selectedTabMiddle'>".
1838                        "&nbsp;<b>$t->[1]</b>&nbsp;</td>";
1839                 $rv .= "<td valign=top $cb class='selectedTabRight'>".
1840                        "<img src=$imgdir/rc2.gif alt=\"\"></td>";
1841                 }
1842         else {
1843                 # Other tab (which has a link)
1844                 $rv .= "<td valign=top $tb>".
1845                        "<img src=$imgdir/lc1.gif alt=\"\"></td>";
1846                 $rv .= "<td $tb nowrap>".
1847                        "&nbsp;<a href='$t->[2]' ".
1848                        "onClick='return select_tab(\"$name\", \"$t->[0]\")'>".
1849                        "$t->[1]</a>&nbsp;</td>";
1850                 $rv .= "<td valign=top $tb>".
1851                        "<img src=$imgdir/rc1.gif ".
1852                        "alt=\"\"></td>";
1853                 $rv .= "</td>\n";
1854                 }
1855         $rv .= "</tr></table>";
1856         $rv .= "</td>\n";
1857         }
1858 $rv .= "<td bgcolor=#ffffff width=1><img src=$imgdir/1x1.gif></td>\n";
1859 $rv .= "</table>\n";
1860
1861 if ($border) {
1862         # All tabs are within a grey box
1863         $rv .= "<table width=100% cellpadding=0 cellspacing=0 border=0 ".
1864                "class='ui_tabs_box'>\n";
1865         $rv .= "<tr> <td bgcolor=#ffffff rowspan=3 width=1><img src=$imgdir/1x1.gif></td>\n";
1866         $rv .= "<td $cb colspan=3 height=2><img src=$imgdir/1x1.gif></td> </tr>\n";
1867         $rv .= "<tr> <td $cb width=2><img src=$imgdir/1x1.gif></td>\n";
1868         $rv .= "<td valign=top>";
1869         }
1870 $main::ui_tabs_selected = $sel;
1871 return $rv;
1872 }
1873
1874 =head2 ui_tabs_end(show-border)
1875
1876 Returns HTML to end a block started by ui_tabs_start. The show-border parameter
1877 must match the parameter with the same name in the start function.
1878
1879 =cut
1880 sub ui_tabs_end
1881 {
1882 return &theme_ui_tabs_end(@_) if (defined(&theme_ui_tabs_end));
1883 my ($border) = @_;
1884 my $rv;
1885 my $imgdir = "$gconfig{'webprefix'}/images";
1886 if ($border) {
1887         $rv .= "</td>\n";
1888         $rv .= "<td $cb width=2><img src=$imgdir/1x1.gif></td>\n";
1889         $rv .= "</tr>\n";
1890         $rv .= "<tr> <td $cb colspan=3 height=2><img src=$imgdir/1x1.gif></td> </tr>\n";
1891         $rv .= "</table>\n";
1892         }
1893 return $rv;
1894 }
1895
1896 =head2 ui_tabs_start_tab(name, tab)
1897
1898 Must be called before outputting the HTML for the named tab, and returns HTML
1899 for the required <div> block. 
1900
1901 =cut
1902 sub ui_tabs_start_tab
1903 {
1904 return &theme_ui_tabs_start_tab(@_) if (defined(&theme_ui_tabs_start_tab));
1905 my ($name, $tab) = @_;
1906 my $defclass = $tab eq $main::ui_tabs_selected ?
1907                         'opener_shown' : 'opener_hidden';
1908 my $rv = "<div id='div_$tab' class='$defclass ui_tabs_start'>\n";
1909 return $rv;
1910 }
1911
1912 =head2 ui_tabs_start_tabletab(name, tab)
1913
1914 Behaves like ui_tabs_start_tab, but for use within a ui_table_start block. 
1915 I recommend against using this where possible, as it is difficult for themes
1916 to implement.
1917
1918 =cut
1919 sub ui_tabs_start_tabletab
1920 {
1921 return &theme_ui_tabs_start_tabletab(@_)
1922         if (defined(&theme_ui_tabs_start_tabletab));
1923 my $div = &ui_tabs_start_tab(@_);
1924 return "</table>\n".$div."<table width=100%>\n";
1925 }
1926
1927 =head2 ui_tabs_end_tab
1928
1929 Returns HTML for the end of a block started by ui_tabs_start_tab.
1930
1931 =cut
1932 sub ui_tabs_end_tab
1933 {
1934 return &theme_ui_tabs_end_tab(@_) if (defined(&theme_ui_tabs_end_tab));
1935 return "</div>\n";
1936 }
1937
1938 =head2 ui_tabs_end_tabletab
1939
1940 Returns HTML for the end of a block started by ui_tabs_start_tabletab.
1941
1942 =cut
1943 sub ui_tabs_end_tabletab
1944 {
1945 return &theme_ui_tabs_end_tabletab(@_)
1946         if (defined(&theme_ui_tabs_end_tabletab));
1947 return "</table></div><table width=100%>\n";
1948 }
1949
1950 =head2 ui_max_text_width(width, [text-area?])
1951
1952 Returns a new width for a text field, based on theme settings. For internal
1953 use only.
1954
1955 =cut
1956 sub ui_max_text_width
1957 {
1958 my ($w, $ta) = @_;
1959 my $max = $ta ? $tconfig{'maxareawidth'} : $tconfig{'maxboxwidth'};
1960 return $max && $w > $max ? $max : $w;
1961 }
1962
1963 ####################### radio hidden functions
1964
1965 =head2 ui_radio_selector(&opts, name, selected)
1966
1967 Returns HTML for a set of radio buttons, each of which shows a different
1968 block of HTML when selected. &opts is an array ref to arrays containing
1969 [ value, label, html ]
1970
1971 XXX
1972
1973 =cut
1974 sub ui_radio_selector
1975 {
1976 return &theme_ui_radio_selector(@_) if (defined(&theme_ui_radio_selector));
1977 my ($opts, $name, $sel) = @_;
1978 my $rv;
1979 if (!$main::ui_radio_selector_donejs++) {
1980         $rv .= &ui_radio_selector_javascript();
1981         }
1982 my $optnames =
1983         "[".join(",", map { "\"".&html_escape($_->[0])."\"" } @$opts)."]";
1984 foreach my $o (@$opts) {
1985         $rv .= &ui_oneradio($name, $o->[0], $o->[1], $sel eq $o->[0],
1986             "onClick='selector_show(\"$name\", \"$o->[0]\", $optnames)'");
1987         }
1988 $rv .= "<br>\n";
1989 foreach my $o (@$opts) {
1990         my $cls = $o->[0] eq $sel ? "selector_shown" : "selector_hidden";
1991         $rv .= "<div id=sel_${name}_$o->[0] class=$cls>".$o->[2]."</div>\n";
1992         }
1993 return $rv;
1994 }
1995
1996 sub ui_radio_selector_javascript
1997 {
1998 return <<EOF;
1999 <style>
2000 .selector_shown {display:inline}
2001 .selector_hidden {display:none}
2002 </style>
2003 <script>
2004 function selector_show(name, value, values)
2005 {
2006 for(var i=0; i<values.length; i++) {
2007         var divobj = document.getElementById('sel_'+name+'_'+values[i]);
2008         divobj.className = value == values[i] ? 'selector_shown'
2009                                               : 'selector_hidden';
2010         }
2011 }
2012 </script>
2013 EOF
2014 }
2015
2016 ####################### grid layout functions
2017
2018 =head2 ui_grid_table(&elements, columns, [width-percent], [tds], [tabletags], [title])
2019
2020 Given a list of HTML elements, formats them into a table with the given
2021 number of columns. However, themes are free to override this to use fewer
2022 columns where space is limited.
2023
2024 =cut
2025 sub ui_grid_table
2026 {
2027 return &theme_ui_grid_table(@_) if (defined(&theme_ui_grid_table));
2028 my ($elements, $cols, $width, $tds, $tabletags, $title) = @_;
2029 return "" if (!@$elements);
2030 my $rv = "<table class='ui_grid_table'".
2031             ($width ? " width=$width%" : "").
2032             ($tabletags ? " ".$tabletags : "").
2033             ">\n";
2034 my $i;
2035 for($i=0; $i<@$elements; $i++) {
2036         $rv .= "<tr class='ui_grid_row'>" if ($i%$cols == 0);
2037         $rv .= "<td ".$tds->[$i%$cols]." valign=top class='ui_grid_cell'>".
2038                $elements->[$i]."</td>\n";
2039         $rv .= "</tr>" if ($i%$cols == $cols-1);
2040         }
2041 if ($i%$cols) {
2042         while($i%$cols) {
2043                 $rv .= "<td ".$tds->[$i%$cols]." class='ui_grid_cell'>".
2044                        "<br></td>\n";
2045                 $i++;
2046                 }
2047         $rv .= "</tr>\n";
2048         }
2049 $rv .= "</table>\n";
2050 if (defined($title)) {
2051         $rv = "<table class=ui_table border ".
2052               ($width ? " width=$width%" : "").">\n".
2053               ($title ? "<tr $tb> <td><b>$title</b></td> </tr>\n" : "").
2054               "<tr $cb> <td>$rv</td> </tr>\n".
2055               "</table>";
2056         }
2057 return $rv;
2058 }
2059
2060 =head2 ui_radio_table(name, selected, &rows)
2061
2062 Returns HTML for a table of radio buttons, each of which has a label and
2063 some associated inputs to the right.
2064
2065 =cut
2066 sub ui_radio_table
2067 {
2068 return &theme_ui_radio_table(@_) if (defined(&theme_ui_radio_table));
2069 my ($name, $sel, $rows) = @_;
2070 return "" if (!@$rows);
2071 my $rv = "<table class='ui_radio_table'>\n";
2072 foreach my $r (@$rows) {
2073         $rv .= "<tr>\n";
2074         $rv .= "<td valign=top".(defined($r->[2]) ? "" : " colspan=2").
2075                "><b>".
2076                &ui_oneradio($name, $r->[0], $r->[1], $r->[0] eq $sel).
2077                "</b></td>\n";
2078         if (defined($r->[2])) {
2079                 $rv .= "<td valign=top>".$r->[2]."</td>\n";
2080                 }
2081         $rv .= "</tr>\n";
2082         }
2083 $rv .= "</table>\n";
2084 return $rv;
2085 }
2086
2087 =head2 ui_up_down_arrows(uplink, downlink, up-show, down-show)
2088
2089 Returns HTML for moving some objects in a table up or down
2090
2091 =cut
2092 sub ui_up_down_arrows
2093 {
2094 return &theme_ui_up_down_arrows(@_) if (defined(&theme_ui_up_down_arrows));
2095 my ($uplink, $downlink, $upshow, $downshow) = @_;
2096 my $mover;
2097 my $imgdir = "$gconfig{'webprefix'}/images";
2098 if ($downshow) {
2099         $mover .= "<a href=\"$downlink\">".
2100                   "<img src=$imgdir/movedown.gif border=0></a>";
2101         }
2102 else {
2103         $mover .= "<img src=$imgdir/movegap.gif>";
2104         }
2105 if ($upshow) {
2106         $mover .= "<a href=\"$uplink\">".
2107                   "<img src=$imgdir/moveup.gif border=0></a>";
2108         }
2109 else {
2110         $mover .= "<img src=$imgdir/movegap.gif>";
2111         }
2112 return $mover;
2113 }
2114
2115 =head2 ui_hr
2116
2117 Returns a horizontal row tag
2118
2119 =cut
2120 sub ui_hr
2121 {
2122 return &theme_ui_hr() if (defined(&theme_ui_hr));
2123 return "<hr>\n";
2124 }
2125
2126 =head2 ui_nav_link(direction, url, disabled)
2127
2128 Returns an arrow icon linking to provided url
2129
2130 =cut
2131 sub ui_nav_link
2132 {
2133 return &theme_ui_nav_link(@_) if (defined(&theme_ui_nav_link));
2134 my ($direction, $url, $disabled) = @_;
2135 my $alt = $direction eq "left" ? '<-' : '->';
2136 if ($disabled) {
2137         return "<img alt=\"$alt\" align=\"middle\""
2138              . "src=\"$gconfig{'webprefix'}/images/$direction-grey.gif\">\n";
2139         }
2140 else {
2141         return "<a href=\"$url\"><img alt=\"$alt\" align=\"middle\""
2142              . "src=\"$gconfig{'webprefix'}/images/$direction.gif\"></a>\n";
2143         }
2144 }
2145
2146 =head2 ui_confirmation_form(cgi, message, &hiddens, [&buttons], [&otherinputs], [extra-warning])
2147
2148 Returns HTML for a form asking for confirmation before performing some
2149 action, such as deleting a user.
2150
2151 =cut
2152 sub ui_confirmation_form
2153 {
2154 my ($cgi, $message, $hiddens, $buttons, $others, $warning) = @_;
2155 my $rv;
2156 $rv .= "<center class=ui_confirmation>\n";
2157 $rv .= &ui_form_start($cgi, "post");
2158 foreach my $h (@$hiddens) {
2159         $rv .= &ui_hidden(@$h);
2160         }
2161 $rv .= "<b>$message</b><p>\n";
2162 if ($warning) {
2163         $rv .= "<b><font color=#ff0000>$warning</font></b><p>\n";
2164         }
2165 if ($others) {
2166         $rv .= $others."<p>\n";
2167         }
2168 $rv .= &ui_form_end($buttons);
2169 $rv .= "</center>\n";
2170 return $rv;
2171 }
2172
2173 ####################### javascript functions
2174
2175 =head2 js_disable_inputs
2176
2177 js_disable_input(&disable-inputs, &enable-inputs, [tag])
2178 Returns Javascript to disable some form elements and enable others
2179
2180 =cut
2181 sub js_disable_inputs
2182 {
2183 my $rv;
2184 my $f;
2185 foreach $f (@{$_[0]}) {
2186         $rv .= "e = form.elements[\"$f\"]; e.disabled = true; ";
2187         $rv .= "for(i=0; i<e.length; i++) { e[i].disabled = true; } ";
2188         }
2189 foreach $f (@{$_[1]}) {
2190         $rv .= "e = form.elements[\"$f\"]; e.disabled = false; ";
2191         $rv .= "for(i=0; i<e.length; i++) { e[i].disabled = false; } ";
2192         }
2193 foreach $f (@{$_[1]}) {
2194         if ($f =~ /^(.*)_def$/ && &indexof($1, @{$_[1]}) >= 0) {
2195                 # When enabling both a _def field and its associated text field,
2196                 # disable the text if the _def is set to 1
2197                 my $tf = $1;
2198                 $rv .= "e = form.elements[\"$f\"]; for(i=0; i<e.length; i++) { if (e[i].checked && e[i].value == \"1\") { form.elements[\"$tf\"].disabled = true } } ";
2199                 }
2200         }
2201 return $_[2] ? "$_[2]='$rv'" : $rv;
2202 }
2203
2204 =head2 ui_page_flipper(message, [inputs, cgi], left-link, right-link, [far-left-link], [far-right-link], [below])
2205
2206 Returns HTML for moving left and right in some large list, such as an inbox
2207 or database table. If only 5 parameters are given, no far links are included.
2208 If any link is undef, that array will be greyed out.
2209
2210 =cut
2211 sub ui_page_flipper
2212 {
2213 return &theme_ui_page_flipper(@_) if (defined(&theme_ui_page_flipper));
2214 my ($msg, $inputs, $cgi, $left, $right, $farleft, $farright, $below) = @_;
2215 my $rv = "<center>";
2216 $rv .= &ui_form_start($cgi) if ($cgi);
2217
2218 # Far left link, if needed
2219 if (@_ > 5) {
2220         if ($farleft) {
2221                 $rv .= "<a href='$farleft'><img src=/images/first.gif ".
2222                        "border=0 align=middle></a>\n";
2223                 }
2224         else {
2225                 $rv .= "<img src=/images/first-grey.gif ".
2226                        "border=0 align=middle></a>\n";
2227                 }
2228         }
2229
2230 # Left link
2231 if ($left) {
2232         $rv .= "<a href='$left'><img src=/images/left.gif ".
2233                "border=0 align=middle></a>\n";
2234         }
2235 else {
2236         $rv .= "<img src=/images/left-grey.gif ".
2237                "border=0 align=middle></a>\n";
2238         }
2239
2240 # Message and inputs
2241 $rv .= $msg;
2242 $rv .= " ".$inputs if ($inputs);
2243
2244 # Right link
2245 if ($right) {
2246         $rv .= "<a href='$right'><img src=/images/right.gif ".
2247                "border=0 align=middle></a>\n";
2248         }
2249 else {
2250         $rv .= "<img src=/images/right-grey.gif ".
2251                "border=0 align=middle></a>\n";
2252         }
2253
2254 # Far right link, if needed
2255 if (@_ > 5) {
2256         if ($farright) {
2257                 $rv .= "<a href='$farright'><img src=/images/last.gif ".
2258                        "border=0 align=middle></a>\n";
2259                 }
2260         else {
2261                 $rv .= "<img src=/images/last-grey.gif ".
2262                        "border=0 align=middle></a>\n";
2263                 }
2264         }
2265
2266 $rv .= "<br>".$below if ($below);
2267 $rv .= &ui_form_end() if ($cgi);
2268 $rv .= "</center>\n";
2269 return $rv;
2270 }
2271
2272 =head2 js_checkbox_disable(name, &checked-disable, &checked-enable, [tag])
2273
2274 MISSING DOCUMENTATION
2275
2276 =cut
2277 sub js_checkbox_disable
2278 {
2279 my $rv;
2280 my $f;
2281 foreach $f (@{$_[1]}) {
2282         $rv .= "form.elements[\"$f\"].disabled = $_[0].checked; ";
2283         }
2284 foreach $f (@{$_[2]}) {
2285         $rv .= "form.elements[\"$f\"].disabled = !$_[0].checked; ";
2286         }
2287 return $_[3] ? "$_[3]='$rv'" : $rv;
2288 }
2289
2290 =head2 js_redirect(url, [window-object])
2291
2292 Returns HTML to trigger a redirect to some URL
2293
2294 =cut
2295 sub js_redirect
2296 {
2297 my ($url, $window) = @_;
2298 if (defined(&theme_js_redirect)) {
2299         return &theme_js_redirect(@_);
2300         }
2301 return "<script>${window}.location = '".&quote_escape($url)."';</script>\n";
2302 }
2303
2304 1;
2305