--- loncom/interface/lonhtmlcommon.pm	2002/07/22 20:35:05	1.1
+++ loncom/interface/lonhtmlcommon.pm	2003/03/10 20:21:45	1.17
@@ -1,17 +1,329 @@
+# The LearningOnline Network with CAPA
+# a pile of common html routines
+#
+# $Id: lonhtmlcommon.pm,v 1.17 2003/03/10 20:21:45 matthew Exp $
+#
+# Copyright Michigan State University Board of Trustees
+#
+# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
+#
+# LON-CAPA is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LON-CAPA is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with LON-CAPA; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# /home/httpd/html/adm/gpl.txt
+#
+# http://www.lon-capa.org/
+#
+######################################################################
+######################################################################
+
+=pod
+
+=head1 NAME
+
+Apache::lonhtmlcommon - routines to do common html things
+
+=head1 SYNOPSIS
+
+Referenced by other mod_perl Apache modules.
+
+=head1 INTRODUCTION
+
+lonhtmlcommon is a collection of subroutines used to present information
+in a consistent html format, or provide other functionality related to
+html.
+
+=head2 General Subroutines
+
+=over 4
+
+=cut 
+
+######################################################################
+######################################################################
+
 package Apache::lonhtmlcommon;
 
+use Time::Local;
 use strict;
 
+##############################################
+##############################################
+
+=pod
+
+=item &date_setter
+
+Inputs
+
+=over 4
+
+=item $dname 
+
+The name to prepend to the form elements.  
+The form elements defined will be dname_year, dname_month, dname_day,
+dname_hour, dname_min, and dname_sec.
+
+=item $currentvalue
+
+The current setting for this time parameter.  A unix format time
+(time in seconds since the beginning of Jan 1st, 1970, GMT.  
+An undefined value is taken to indicate the value is the current time.
+Also, to be explicit, a value of 'now' also indicates the current time.
+
+=cut
+
+##############################################
+##############################################
+sub date_setter {
+    my ($formname,$dname,$currentvalue) = @_;
+    if (! defined($currentvalue) || $currentvalue eq 'now') {
+        $currentvalue = time;
+    }
+    # other potentially useful values:     wkday,yrday,is_daylight_savings
+    my ($sec,$min,$hour,$mday,$month,$year,undef,undef,undef) = 
+        localtime($currentvalue);
+    $year += 1900;
+    my $result = "\n<!-- $dname date setting form -->\n";
+    $result .= <<ENDJS;
+<script language="Javascript">
+    function $dname\_checkday() {
+        var day   = document.$formname.$dname\_day.value;
+        var month = document.$formname.$dname\_month.value;
+        var year  = document.$formname.$dname\_year.value;
+        var valid = true;
+        if (day < 1) {
+            document.$formname.$dname\_day.value = 1;
+        } 
+        if (day > 31) {
+            document.$formname.$dname\_day.value = 31;
+        }
+        if ((month == 1)  || (month == 3)  || (month == 5)  ||
+            (month == 7)  || (month == 8)  || (month == 10) ||
+            (month == 12)) {
+            if (day > 31) {
+                document.$formname.$dname\_day.value = 31;
+                day = 31;
+            }
+        } else if (month == 2 ) {
+            if ((year % 4 == 0) && (year % 100 != 0)) {
+                if (day > 29) {
+                    document.$formname.$dname\_day.value = 29;
+                }
+            } else if (day > 29) {
+                document.$formname.$dname\_day.value = 28;
+            }
+        } else if (day > 30) {
+            document.$formname.$dname\_day.value = 30;
+        }
+    }
+</script>
+ENDJS
+    $result .= "  <select name=\"$dname\_month\" ".
+        "onChange=\"javascript:$dname\_checkday()\" >\n";
+    my @Months = qw/January February  March     April   May      June 
+                    July    August    September October November December/;
+    # Pad @Months with a bogus value to make indexing easier
+    unshift(@Months,'If you can read this an error occurred');
+    for(my $m = 1;$m <=$#Months;$m++) {
+        $result .= "      <option value=\"$m\" ";
+        $result .= "selected " if ($m-1 == $month);
+        $result .= "> $Months[$m] </option>\n";
+    }
+    $result .= "  </select>\n";
+    $result .= "  <input type=\"text\" name=\"$dname\_day\" ".
+            "value=\"$mday\" size=\"3\" ".
+            "onChange=\"javascript:$dname\_checkday()\" />\n";
+    $result .= "  <input type=\"year\" name=\"$dname\_year\" ".
+            "value=\"$year\" size=\"5\" ".
+            "onChange=\"javascript:$dname\_checkday()\" />\n";
+    $result .= "&nbsp;&nbsp;";
+    $result .= "  <select name=\"$dname\_hour\" >\n";
+    for (my $h = 0;$h<24;$h++) {
+        $result .= "      <option value=\"$h\" ";
+        $result .= "selected " if ($hour == $h);
+        $result .= "> ";
+        if ($h == 0) {
+            $result .= "12 am";
+        } elsif($h == 12) {
+            $result .= "12 noon";
+        } elsif($h < 12) {
+            $result .= "$h am";
+        } else {
+            $result .= $h-12 ." pm";
+        }
+        $result .= " </option>\n";
+    } 
+    $result .= "  </select>\n";
+    $result .= "  <input type=\"text\" name=\"$dname\_minute\" ".
+        "value=\"$min\" size=\"3\" /> m\n";
+    $result .= "  <input type=\"text\" name=\"$dname\_second\" ".
+        "value=\"$sec\" size=\"3\" /> s\n";
+    $result .= "<!-- end $dname date setting form -->\n";
+    return $result;
+}
+
+##############################################
+##############################################
+
+=item &get_date_from_form
+
+Inputs:
+
+=over 4
+
+=item $dname
+
+The name passed to &datesetter, which prefixes the form elements.
+
+=item $defaulttime
+
+The unix time to use as the default in case of poor inputs.
+
+=back
+
+Returns: Unix time represented in the form.
+
+=cut
+
+##############################################
+##############################################
+sub get_date_from_form {
+    my ($dname) = @_;
+    my ($sec,$min,$hour,$day,$month,$year);
+    #
+    if (defined($ENV{'form.'.$dname.'_second'})) {
+        my $tmpsec = $ENV{'form.'.$dname.'_second'};
+        if (($tmpsec =~ /^\d+$/) && ($tmpsec >= 0) && ($tmpsec < 60)) {
+            $sec = $tmpsec;
+        }
+    }
+    if (defined($ENV{'form.'.$dname.'_minute'})) {
+        my $tmpmin = $ENV{'form.'.$dname.'_minute'};
+        if (($tmpmin =~ /^\d+$/) && ($tmpmin >= 0) && ($tmpmin < 60)) {
+            $min = $tmpmin;
+        }
+    }
+    if (defined($ENV{'form.'.$dname.'_hour'})) {
+        my $tmphour = $ENV{'form.'.$dname.'_hour'};
+        if (($tmphour =~ /^\d+$/) && ($tmphour > 0) && ($tmphour < 32)) {
+            $hour = $tmphour;
+        }
+    }
+    if (defined($ENV{'form.'.$dname.'_day'})) {
+        my $tmpday = $ENV{'form.'.$dname.'_day'};
+        if (($tmpday =~ /^\d+$/) && ($tmpday > 0) && ($tmpday < 32)) {
+            $day = $tmpday;
+        }
+    }
+    if (defined($ENV{'form.'.$dname.'_month'})) {
+        my $tmpmonth = $ENV{'form.'.$dname.'_month'};
+        if (($tmpmonth =~ /^\d+$/) && ($tmpmonth > 0) && ($tmpmonth < 13)) {
+            $month = $tmpmonth - 1;
+        }
+    }
+    if (defined($ENV{'form.'.$dname.'_year'})) {
+        my $tmpyear = $ENV{'form.'.$dname.'_year'};
+        if (($tmpyear =~ /^\d+$/) && ($tmpyear > 1900)) {
+            $year = $tmpyear - 1900;
+        }
+    }
+    if (eval(&timelocal($sec,$min,$hour,$day,$month,$year))) {
+        return &timelocal($sec,$min,$hour,$day,$month,$year);
+    } else {
+        return undef;
+    }
+}
+
+##############################################
+##############################################
+
+=pod
+
+=item &javascript_nothing()
+
+Return an appropriate null for the users browser.  This is used
+as the first arguement for window.open calls when you want a blank
+window that you can then write to.
+
+=cut
+
+##############################################
+##############################################
+sub javascript_nothing {
+    # mozilla and other browsers work with "''", but IE on mac does not.
+    my $nothing = "''";
+    my $user_browser;
+    my $user_os;
+    $user_browser = $ENV{'browser.type'} if (exists($ENV{'browser.type'}));
+    $user_os      = $ENV{'browser.os'}   if (exists($ENV{'browser.os'}));
+    if (! defined($user_browser) || ! defined($user_os)) {
+        (undef,$user_browser,undef,undef,undef,$user_os) = 
+                           &Apache::loncommon::decode_user_agent();
+    }
+    &Apache::lonnet::logthis(" os      = :".$user_os.":");
+    &Apache::lonnet::logthis(" browser = :".$user_browser.":");
+    if ($user_browser eq 'explorer' && $user_os =~ 'mac') {
+        $nothing = "'javascript:void(0);'";
+    }
+    return $nothing;
+}
+
+##############################################
+##############################################
+
+
+
+sub AscendOrderOptions {
+    my ($order, $page, $formName)=@_;
+
+    my $OpSel1 = '';
+    my $OpSel2 = '';
+
+    if($order eq 'Ascending') {
+        $OpSel1 = ' selected';
+    } else {
+        $OpSel2 = ' selected';
+    }
+
+    my $Str = '';
+    $Str .= '<select name="'.(($page)?$page:'').'Ascend"';
+    if($formName) {
+        $Str .= ' onchange="document.'.$formName.'.submit()"';
+    }
+    $Str .= '>'."\n";
+    $Str .= '<option'.$OpSel1.'>Ascending</option>'."\n".
+	    '<option'.$OpSel2.'>Descending</option>'."\n";
+    $Str .= '</select>'."\n";
+
+    return $Str;
+}
+
 sub MapOptions {
-    my ($data, $page)=@_;
+    my ($data, $page, $formName)=@_;
     my $Str = '';
     $Str .= '<select name="';
-    $Str .= (($page)?$page:'').'Map">'."\n";
+    $Str .= (($page)?$page:'').'Maps"';
+    if($formName) {
+        $Str .= ' onchange="document.'.$formName.'.submit()"';
+    }
+    $Str .= '>'."\n";
 
     my $selected = 0;
     foreach my $sequence (split(':',$data->{'orderedSequences'})) {
 	$Str .= '<option';
-        if($data->{$page.'Map'} eq $data->{$sequence.':title'}) {
+        if($data->{$page.'Maps'} eq $data->{$sequence.':title'}) {
             $Str .= ' selected';
             $selected = 1;
         }
@@ -28,19 +340,87 @@ sub MapOptions {
     return $Str;
 }
 
-sub StudentOptions {
-    my ($cache, $students, $selectedName, $page)=@_;
+sub ProblemOptions {
+    my ($data, $page, $map, $formName)=@_;
+    my $Str = '';
+    $Str .= '<select name="';
+    $Str .= (($page)?$page:'').'ProblemSelect"';
+    if($formName) {
+        $Str .= ' onchange="document.'.$formName.'.submit()"';
+    }
+    $Str .= '>'."\n";
+
+    my $selected = 0;
+    foreach my $sequence (split(':',$data->{'orderedSequences'})) {
+	if($data->{$sequence.':title'} eq $map || $map eq 'All Maps') {
+	    foreach my $problem (split(':', $data->{$sequence.':problems'})) {
+		$Str .= '<option';
+		if($data->{$page.'ProblemSelect'} eq 
+		   $data->{$problem.':title'}) {
+		    $Str .= ' selected';
+		    $selected = 1;
+		}
+		$Str .= '>'.$data->{$problem.':title'}.'</option>'."\n";
+	    }
+	}
+    }
+    $Str .= '<option';
+    if(!$selected) {
+        $Str .= ' selected';
+    }
+    $Str .= '>All Problems</option>'."\n";
+
+    $Str .= '</select>'."\n";
+
+    return $Str;
+}
 
+sub PartOptions {
+    my ($data, $page, $parts, $formName)=@_;
     my $Str = '';
-    $Str = '<select name="'.(($page)?$page:'').'Student">'."\n";
 
-    my $selected=0;
+    if(!defined($parts)) {
+	return '';
+    }
+
+    $Str .= '<select name="';
+    $Str .= (($page)?$page:'').'PartSelect"';
+    if($formName) {
+        $Str .= ' onchange="document.'.$formName.'.submit()"';
+    }
+    $Str .= '>'."\n";
+
+    my $selected = 0;
+    foreach my $part (@$parts) {
+	$Str .= '<option';
+	if($data->{$page.'PartSelect'} eq $part) {
+	    $Str .= ' selected';
+	    $selected = 1;
+	}
+	$Str .= '>'.$part.'</option>'."\n";	     
+    }
     $Str .= '<option';
-    if($selectedName eq 'All Students') {
+    if(!$selected) {
         $Str .= ' selected';
-        $selected = 1;
     }
-    $Str .= '>All Students</option>'."\n";
+    $Str .= '>All Parts</option>'."\n";
+
+    $Str .= '</select>'."\n";
+
+    return $Str;
+}
+
+sub StudentOptions {
+    my ($cache, $students, $selectedName, $page, $formName)=@_;
+
+    my $Str = '';
+    $Str .= '<select name="'.(($page)?$page:'').'Student"';
+    if($formName) {
+        $Str .= ' onchange="document.'.$formName.'.submit()"';
+    }
+    $Str .= '>'."\n";
+
+    my $selected=0;
 
     foreach (@$students) {
 	$Str .= '<option';
@@ -54,11 +434,18 @@ sub StudentOptions {
     }
 
     $Str .= '<option';
-    if(!$selected) {
+    if($selectedName eq 'No Student Selected') {
         $Str .= ' selected';
+        $selected = 1;
     }
     $Str .= '>No Student Selected</option>'."\n";
 
+    $Str .= '<option';
+    if(!$selected) {
+        $Str .= ' selected';
+    }
+    $Str .= '>All Students</option>'."\n";
+
     $Str .= '</select>'."\n";
 
     return $Str;
@@ -87,77 +474,103 @@ sub StatusOptions {
     $Str .= '</select>'."\n";
 }
 
-sub Title {
-    my ($pageName)=@_;
 
-    my $Str = '';
+########################################################
+########################################################
 
-    $Str .= '<html><head><title>'.$pageName.'</title></head>'."\n";
-    $Str .= '<body bgcolor="#FFFFFF">'."\n";
-    $Str .= '<script>window.focus(); window.width=500;window.height=500;';
-    $Str .= '</script>'."\n";
-    $Str .= '<table width="100%"><tr><td valign="top">';
-    $Str .= '<h1> Course: ';
-    $Str .= $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
-    $Str .= '</h1></td><td align="right">'."\n";
-    $Str .= '<img align="right" src=/adm/lonIcons/lonlogos.gif>';
-    $Str .= '</td></tr></table>'."\n";
-#    $Str .= '<h3>Current Time: '.localtime(time).'</h3><br><br><br>'."\n";
+=pod
 
-    return $Str;
-}
+=item &MultipleSectionSelect()
 
-sub CreateStatisticsMainMenu {
-    my ($status, $reports)=@_;
+Inputs: 
 
-    my $Str = '';
+=over 4
+
+=item $sections A references to an array containing the names of all the
+sections used in a class.
+
+=item $selectedSections A reference to an array containing the names of the
+currently selected sections.
 
-    $Str .= '<table border="0"><tbody><tr>'."\n";
-    $Str .= '<td></td><td></td>'."\n";
-    $Str .= '<td align="center"><b>Analysis Reports:</b></td>'."\n";
-    $Str .= '<td align="center"><b>Student Status:</b></td></tr>'."\n";
-    $Str .= '<tr>'."\n";
-    $Str .= '<td align="center"><input type="submit" name="Refresh" ';
-    $Str .= 'value="Refresh" /></td>'."\n";
-    $Str .= '<td align="center"><input type="submit" name="DownloadAll" ';
-    $Str .= 'value="Update All Student Data" /></td>'."\n";
-    $Str .= '<td align="center">';
-    $Str .= '<select name="reportSelected" onchange="document.';
-    $Str .= 'Statistics.submit()">'."\n";
+=back 
 
-    foreach (sort(keys(%$reports))) {
-        next if($_ eq 'reportSelected');
-        $Str .= '<option name="'.$_.'"';
-        if($reports->{'reportSelected'} eq $reports->{$_}) {
-            $Str .= ' selected=""';
+Returns: a string containing HTML for a multiple select box for
+selecting sections of a course.  
+
+The form element name is 'Section'.  @$sections is sorted prior to output.
+
+=cut
+
+########################################################
+########################################################
+sub MultipleSectionSelect {
+    my ($sections,$selectedSections)=@_;
+
+    my $Str = '';
+    $Str .= '<select name="Section" multiple="true" size="4">'."\n";
+
+    foreach (sort @$sections) {
+        $Str .= '<option';
+        foreach my $selected (@$selectedSections) {
+            if($_ eq $selected) {
+                $Str .= ' selected=""';
+            }
         }
-        $Str .= '>'.$reports->{$_}.'</option>'."\n";
+        $Str .= '>'.$_.'</option>'."\n";
     }
-    $Str .= '</select></td>'."\n";
+    $Str .= '</select>'."\n";
+    
+    return $Str;
+}
+
+########################################################
+########################################################
+
+=pod
+
+=item &Title()
+
+Inputs: $pageName a string containing the name of the page to be sent
+to &Apache::loncommon::bodytag.
+
+Returns: string containing being <html> and complete <head> and <title>
+as well as a <script> to focus the current window and change its width
+and height to 500.  Why?  I do not know.  If you find out, please update
+this documentation.
 
-    $Str .= '<td align="center">';
-    $Str .= &StatusOptions($status, 'Statistics');
-    $Str .= '</td>'."\n";
+=cut
+
+########################################################
+########################################################
+sub Title {
+    my ($pageName)=@_;
+
+    my $Str = '';
 
-    $Str .= '</tr></tbody></table>'."\n";
-    $Str .= '<hr>'."\n";
+    $Str .= '<html><head><title>'.$pageName.'</title></head>'."\n";
+    $Str .= &Apache::loncommon::bodytag($pageName)."\n";
+    $Str .= '<script>window.focus(); window.width=500;window.height=500;';
+    $Str .= '</script>'."\n";
 
     return $Str;
 }
 
+########################################################
+########################################################
+
 =pod
 
-=item &CreateTableHeadings()
+=item &CreateHeadings()
 
 This function generates the column headings for the chart.
 
 =over 4
 
-Inputs: $CacheData, $studentInformation, $headings, $spacePadding
+Inputs: $CacheData, $keyID, $headings, $spacePadding
 
 $CacheData: pointer to a hash tied to the cached data database
 
-$studentInformation: a pointer to an array containing the names of the data 
+$keyID: a pointer to an array containing the names of the data 
 held in a column and is used as part of a key into $CacheData
 
 $headings: The names of the headings for the student information
@@ -172,41 +585,53 @@ $Str: A formatted string of the table co
 
 =cut
 
-sub CreateStudentInformationHeadings {
-    my ($data,$studentInformation,$headings,$displayString)=@_;
+########################################################
+########################################################
+sub CreateHeadings {
+    my ($data,$keyID,$headings,$displayString,$format)=@_;
     my $Str='';
+    my $formatting = '';
 
     for(my $index=0; $index<(scalar @$headings); $index++) {
-#        if(!&ShouldShowColumn($data, 'ChartHeading'.$index)) {
-#            next;
-#        }
- 	my $data=$headings->[$index];
-        my $linkdata=$studentInformation->[$index];
+ 	my $currentHeading=$headings->[$index];
+        if($format eq 'preformatted') {
+            my @dataLength=split(//,$currentHeading);
+            my $length=scalar @dataLength;
+            $formatting = (' 'x
+                      ($data->{$keyID->[$index].':columnWidth'}-$length));
+        }
+        my $linkdata=$keyID->[$index];
+
         my $tempString = $displayString;
         $tempString =~ s/LINKDATA/$linkdata/;
-        $tempString =~ s/DISPLAYDATA/$data/;
+        $tempString =~ s/DISPLAYDATA/$currentHeading/;
+        $tempString =~ s/FORMATTING/$formatting/;
+
         $Str .= $tempString;
     }
 
     return $Str;
 }
 
+########################################################
+########################################################
+
 =pod
 
 =item &FormatStudentInformation()
 
-This function produces a formatted string of the student's information:
+This function produces a formatted string of the student\'s information:
 username, domain, section, full name, and PID.
 
 =over 4
 
-Input: $cache, $name, $studentInformation, $spacePadding
+Input: $cache, $name, $keyID, $spacePadding
 
 $cache: This is a pointer to a hash that is tied to the cached data
 
 $name:  The name and domain of the current student in name:domain format
 
-$studentInformation: A pointer to an array holding the names used to
+$keyID: A pointer to an array holding the names used to
 
 remove data from the hash.  They represent the name of the data to be removed.
 
@@ -220,27 +645,103 @@ $Str: Formatted string.
 
 =cut
 
+########################################################
+########################################################
 sub FormatStudentInformation {
-    my ($cache,$name,$studentInformation,$spacePadding)=@_;
+    my ($data,$name,$keyID,$displayString,$format)=@_;
     my $Str='';
-    my $data;
+    my $currentColumn;
+
+    for(my $index=0; $index<(scalar @$keyID); $index++) {
+        $currentColumn=$data->{$name.':'.$keyID->[$index]};
+
+        if($format eq 'preformatted') {
+            my @dataLength=split(//,$currentColumn);
+            my $length=scalar @dataLength;
+            $currentColumn.= (' 'x
+                     ($data->{$keyID->[$index].':columnWidth'}-$length));
+        }
+
+        my $tempString = $displayString;
+        $tempString =~ s/DISPLAYDATA/$currentColumn/;
 
-    for(my $index=0; $index<(scalar @$studentInformation); $index++) {
-        if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) {
-            next;
-        }
-        $data=$cache->{$name.':'.$studentInformation->[$index]};
-	$Str .= $data;
-
-	my @dataLength=split(//,$data);
-	my $length=scalar @dataLength;
-	$Str .= (' 'x($cache->{$studentInformation->[$index].'Length'}-
-                      $length));
-	$Str .= $spacePadding;
+        $Str .= $tempString;
     }
 
     return $Str;
 }
 
+########################################################
+########################################################
+
+# Create progress
+sub Create_PrgWin {
+    my ($r, $title, $heading, $number_to_do)=@_;
+    $r->print('<script>'.
+    "popwin=open(\'\',\'popwin\',\'width=400,height=100\');".
+    "popwin.document.writeln(\'<html><head><title>$title</title></head>".
+	      "<body bgcolor=\"#88DDFF\">".
+              "<h4>$heading</h4>".
+              "<form name=popremain>".
+              "<input type=text size=55 name=remaining value=Starting></form>".
+              "</body></html>\');".
+    "popwin.document.close();".
+    "</script>");
+
+    my %prog_state;
+    $prog_state{'done'}=0;
+    $prog_state{'firststart'}=time;
+    $prog_state{'laststart'}=time;
+    $prog_state{'max'}=$number_to_do;
+
+    $r->rflush();
+    return %prog_state;
+}
+
+# update progress
+sub Update_PrgWin {
+    my ($r,$prog_state,$displayString)=@_;
+    $r->print('<script>popwin.document.popremain.remaining.value="'.
+              $displayString.'";</script>');
+    $$prog_state{'laststart'}=time;
+    $r->rflush();
+}
+
+# increment progress state
+sub Increment_PrgWin {
+    my ($r,$prog_state,$extraInfo)=@_;
+    $$prog_state{'done'}++;
+    my $time_est= (time - $$prog_state{'firststart'})/$$prog_state{'done'} *
+	($$prog_state{'max'}-$$prog_state{'done'});
+    $time_est = int($time_est);
+    if (int ($time_est/60) > 0) {
+	my $min = int($time_est/60);
+	my $sec = $time_est % 60;
+	$time_est = $min.' minutes';
+	if ($sec > 1) {
+	    $time_est.= ', '.$sec.' seconds';
+	} elsif ($sec > 0) {
+	    $time_est.= ', '.$sec.' second';
+	}
+    } else {
+	$time_est .= ' seconds';
+    }
+
+    $r->print('<script>popwin.document.popremain.remaining.value="'.
+	      $$prog_state{'done'}.'/'.$$prog_state{'max'}.
+	      ': '.$time_est.' remaining ('.(time-$$prog_state{'laststart'}).
+	      ' seconds for '.$extraInfo.')";'.'</script>');
+    $$prog_state{'laststart'}=time;
+    $r->rflush();
+}
+
+# close Progress Line
+sub Close_PrgWin {
+    my ($r,$prog_state)=@_;
+    $r->print('<script>popwin.close()</script>'."\n");
+    undef(%$prog_state);
+    $r->rflush(); 
+}
+
 1;
 __END__