liquid_feedback_frontend
view static/gregor.js/gregor.js @ 1587:d5c5bf6bfda5
Show action element in issues only if necessary
| author | bsw | 
|---|---|
| date | Wed Jan 27 09:57:10 2021 +0100 (2021-01-27) | 
| parents | 77260f05fd4b | 
| children | 
 line source
     1 //
     2 // Copyright (c) 2009 Public Software Group e. V., Berlin
     3 //
     4 // Permission is hereby granted, free of charge, to any person obtaining a
     5 // copy of this software and associated documentation files (the
     6 // "Software"), to deal in the Software without restriction, including 
     7 // without limitation the rights to use, copy, modify, merge, publish,
     8 // distribute, sublicense, and/or sell copies of the Software, and to
     9 // permit persons to whom the Software is furnished to do so, subject to
    10 // the following conditions:
    11 //
    12 // The above copyright notice and this permission notice shall be included
    13 // in all copies or substantial portions of the Software.
    14 //
    15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
    16 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    18 // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    19 // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    20 // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    21 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    22 //
    24 //
    25 // All date calculations are based on the gregorian calender, also for
    26 // dates before 1582 (before the gegorian calendar was introduced).
    27 // The supported range is from January 1st 0001 up to December 31st 9999.
    28 //
    29 // gregor_daycount({year: <year>, month: <month>, day: <day>}) returns
    30 // the number of days between the given date and January 1st 0001 (greg.).
    31 //
    32 // gregor_completeDate({year: <year>, month: <month>, day: <day>}) returns
    33 // a structure (an object) with the following properties:
    34 //   - daycount     (days since January 1st 0001, see gregor_daycount)
    35 //   - year         (with century)
    36 //   - month        (from 1 to 12)
    37 //   - day          (from 1 to 28, 29, 30 or 31)
    38 //   - iso_string   (string with format YYYY-MM-DD)
    39 //   - us_weekday   (from 0 for Sunday to 6 for Monday)
    40 //   - iso_weekday  (from 0 for Monday to 6 for Sunday)
    41 //   - iso_weekyear (Year containing greater part of week st. w. Monday)
    42 //   - iso_week     (from 1 to 52 or 53)
    43 //   - us_week      (from 1 to 53 or 54)
    44 //
    45 // The structure (the object) passed as parameter to gregor_daycount or
    46 // gregor_completeDate may describe a date in the following ways:
    47 //   - daycount
    48 //   - year, month, day
    49 //   - year, us_week, us_weekday
    50 //   - year, iso_week, iso_weekday
    51 //   - iso_weekyear, iso_week, iso_weekday
    52 //
    53 // gregor_sheet({...}) returns a calendar sheet as DOM object. The
    54 // structure (the object) passed to the function as an argument is altered
    55 // by the function and used to store state variables. Initially it can
    56 // contain the following fields:
    57 //   - year            (year to show, defaults to todays year)
    58 //   - month           (month to show, defaults to todays month)
    59 //   - today           (structure describing a day, e.g. year, month, day)
    60 //   - selected        (structure describing a day, e.g. year, month, day)
    61 //   - navigation      ("enabled", "disabled", "hidden", default "enabled")
    62 //   - week_mode       ("iso" or "us", defaults to "iso")
    63 //   - week_numbers    ("left", "right" or null, defaults to null)
    64 //   - month_names     (e.g. ["January", "Feburary", ...])
    65 //   - weekday_names   (e.g. ["Mon", "Tue", ...] or ["Sun", "Mon", ...])
    66 //   - day_callback    (function to render a cell)
    67 //   - select_callback (function to be called, when a date was selected)
    68 //   - element         (for internal use only)
    69 // If "today" is undefined, it is automatically intitialized with the
    70 // current clients date. If "selected" is undefined or null, no date is
    71 // be initially selected. It is mandatory to provide month_names and
    72 // weekday_names.
    73 //
    74 // gregor_addGui({...}) alters a referenced input field in a way that
    75 // focussing on it causes a calendar being shown at its right side, where a
    76 // date can be selected. The structure (the object) passed to this function
    77 // is only evaluated once, and never modified. All options except "element"
    78 // of the gregor_sheet({...}) function may be used as options to
    79 // gregor_addGui({...}) as well. In addition an "element_id" must be
    80 // provided as argument, containing the id of a text input field. The
    81 // behaviour caused by the options "selected" and  "select_callback" are
    82 // slightly different: If "selected" === undefined, then the current value
    83 // of the text field referenced by "element_id" will be parsed and used as
    84 // date selection. If "selected" === null, then no date will be initially
    85 // selected, and the text field will be cleared. The "select_callback"
    86 // function is always called once with the pre-selected date (or with null,
    87 // if no date is initially selected). Whenever the selected date is changed
    88 // or unselected later, the callback function is called again with the new
    89 // date (or with null, in case of deselection). When the relaxed argument is set
    90 // the calendar will not normalize the parsed date. Thats usefull if you wan't to
    91 // allow relaxed input.
    92 //
    93 // EXAMPLE:
    94 //
    95 // <input type="text" id="test_field" name="test_field" value=""/>
    96 // <script type="text/javascript">
    97 //   gregor_addGui({
    98 //     element_id: 'test_field',
    99 //     month_names: [
   100 //       "January", "February", "March", "April", "May", "June",
   101 //       "July", "August", "September", "October", "November", "December"
   102 //     ],
   103 //     weekday_names: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
   104 //     week_numbers: "left"
   105 //   });
   106 // </script>
   107 //
   112 // Internal constants and helper functions for date calculation
   115 gregor_c1   = 365;                  // days of a non-leap year
   116 gregor_c4   = 4 * gregor_c1 + 1;    // days of a full 4 year cycle
   117 gregor_c100 = 25 * gregor_c4 - 1;   // days of a full 100 year cycle
   118 gregor_c400 = 4 * gregor_c100 + 1;  // days of a full 400 year cycle
   120 gregor_normalMonthOffsets = [
   121   0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
   122 ];
   124 function gregor_getMonthOffset(year, month) {
   125   if (
   126     (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) &&
   127     month > 2
   128   ) return gregor_normalMonthOffsets[month-1] + 1;
   129   else return gregor_normalMonthOffsets[month-1];
   130 }
   132 function gregor_formatInteger(int, digits) {
   133   var result = int.toFixed();
   134   if (digits != null) {
   135     while (result.length < digits) result = "0" + result;
   136   }
   137   return result;
   138 }
   142 // Calculate days since January 1st 0001 (Gegorian) for a given date
   145 function gregor_daycount(obj) {
   146   if (
   147     obj.daycount >= 0 && obj.daycount <= 3652058 && obj.daycount % 1 == 0
   148   ) {
   149     return obj.daycount;
   150   } else if (
   151     obj.year >= 1 && obj.year <= 9999 && obj.year % 1 == 0 &&
   152     obj.month >= 1 && obj.month <= 12 && obj.month % 1 == 0 &&
   153     obj.day >= 0 && obj.day <= 31 && obj.day % 1 == 0
   154   ) {
   155     var n400 = Math.floor((obj.year-1) / 400);
   156     var r400 = (obj.year-1) % 400;
   157     var n100 = Math.floor(r400 / 100);
   158     var r100 = r400 % 100;
   159     var n4 = Math.floor(r100 / 4);
   160     var n1 = r100 % 4;
   161     return (
   162       gregor_c400 * n400 +
   163       gregor_c100 * n100 +
   164       gregor_c4   * n4   +
   165       gregor_c1   * n1   +
   166       gregor_getMonthOffset(obj.year, obj.month) + (obj.day - 1)
   167     );
   168   } else if (
   169     (
   170       (
   171         obj.year >= 1 && obj.year <= 9999 &&
   172         obj.year % 1 == 0 && obj.iso_weekyear == null
   173       ) || (
   174         obj.iso_weekyear >= 1 && obj.iso_weekyear <= 9999 &&
   175         obj.iso_weekyear % 1 == 0 && obj.year == null
   176       )
   177     ) &&
   178     obj.iso_week >= 0 && obj.iso_week <= 53 && obj.iso_week % 1 == 0 &&
   179     obj.iso_weekday >= 0 && obj.iso_weekday <= 6 &&
   180     obj.iso_weekday % 1 == 0
   181   ) {
   182     var jan4th = gregor_daycount({
   183       year:  (obj.year != null) ? obj.year : obj.iso_weekyear,
   184       month: 1,
   185       day:   4
   186     });
   187     var monday0 = jan4th - (jan4th % 7) - 7;  // monday of week 0
   188     return monday0 + 7 * obj.iso_week + obj.iso_weekday;
   189   } else if (
   190     obj.year >= 1 && obj.year <= 9999 && obj.year % 1 == 0 &&
   191     obj.us_week >= 0 && obj.us_week <= 54 && obj.us_week % 1 == 0 &&
   192     obj.us_weekday >= 0 && obj.us_weekday <= 6 && obj.us_weekday % 1 == 0
   193   ) {
   194     var jan1st = gregor_daycount({
   195       year:  obj.year,
   196       month: 1,
   197       day:   1
   198     });
   199     var sunday0 = jan1st - ((jan1st+1) % 7) - 7;  // sunday of week 0
   200     return sunday0 + 7 * obj.us_week + obj.us_weekday;
   201   }
   202 }
   206 // Calculate all calendar related numbers for a given date
   209 function gregor_completeDate(obj) {
   210   if (
   211     obj.daycount >= 0 && obj.daycount <= 3652058 && obj.daycount % 1 == 0
   212   ) {
   213     var daycount = obj.daycount;
   214     var n400 = Math.floor(daycount / gregor_c400);
   215     var r400 = daycount % gregor_c400;
   216     var n100 = Math.floor(r400 / gregor_c100);
   217     var r100 = r400 % gregor_c100;
   218     if (n100 == 4) { n100 = 3; r100 = gregor_c100; }
   219     var n4 = Math.floor(r100 / gregor_c4);
   220     var r4 = r100 % gregor_c4;
   221     var n1 = Math.floor(r4 / gregor_c1);
   222     var r1 = r4 % gregor_c1;
   223     if (n1 == 4) { n1 = 3; r1 = gregor_c1; }
   224     var year = 1 + 400 * n400 + 100 * n100 + 4 * n4 + n1;
   225     var month = 1 + Math.floor(r1 / 31);
   226     var monthOffset = gregor_getMonthOffset(year, month);
   227     if (month < 12) {
   228       var nextMonthOffset = gregor_getMonthOffset(year, month + 1);
   229       if (r1 >= nextMonthOffset) {
   230         month++;
   231         monthOffset = nextMonthOffset;
   232       }
   233     }
   234     var day = 1 + r1 - monthOffset;
   235     var iso_string = ("" +
   236       gregor_formatInteger(year, 4) + "-" +
   237       gregor_formatInteger(month, 2) + "-" +
   238       gregor_formatInteger(day, 2)
   239     );
   240     var us_weekday = (daycount+1) % 7;
   241     var iso_weekday = daycount % 7;
   242     var iso_weekyear = year;
   243     if (
   244       month == 1 && (
   245         (day == 3 && iso_weekday == 6) ||
   246         (day == 2 && iso_weekday >= 5) ||
   247         (day == 1 && iso_weekday >= 4)
   248       )
   249     ) iso_weekyear--;
   250     else if (
   251       month == 12 && (
   252         (day == 29 && iso_weekday == 0) ||
   253         (day == 30 && iso_weekday <= 1) ||
   254         (day == 31 && iso_weekday <= 2)
   255       )
   256     ) iso_weekyear++;
   257     var jan4th = gregor_daycount({year: iso_weekyear, month: 1, day: 4});
   258     var monday0 = jan4th - (jan4th % 7) - 7;  // monday of week 0
   259     var iso_week = Math.floor((daycount - monday0) / 7);
   260     var jan1st = gregor_daycount({year: year, month: 1, day: 1});
   261     var sunday0 = jan1st - ((jan1st+1) % 7) - 7;  // sunday of week 0
   262     var us_week = Math.floor((daycount - sunday0) / 7);
   263     return {
   264       daycount:     daycount,
   265       year:         year,
   266       month:        month,
   267       day:          day,
   268       iso_string:   iso_string,
   269       us_weekday:   (daycount+1) % 7,  // 0 = Sunday
   270       iso_weekday:  daycount % 7,      // 0 = Monday
   271       iso_weekyear: iso_weekyear,
   272       iso_week:     iso_week,
   273       us_week:      us_week
   274     };
   275   } else if (obj.daycount == null) {
   276     var daycount = gregor_daycount(obj);
   277     if (daycount != null) {
   278       return gregor_completeDate({daycount: gregor_daycount(obj)});
   279     }
   280   }
   281 }
   285 // Test gregor_daycount and gregor_completeDate for consistency
   286 // (Debugging only)
   289 function gregor_test() {
   290   for (i=650000; i<900000; i++) {
   291     var obj = gregor_completeDate({daycount: i});
   292     var j;
   293     j = gregor_daycount({
   294       year: obj.year,
   295       month: obj.month,
   296       day: obj.day
   297     });
   298     if (i != j) { alert("ERROR"); return; }
   299     j = gregor_daycount({
   300       iso_weekyear: obj.iso_weekyear,
   301       iso_week:     obj.iso_week,
   302       iso_weekday:  obj.iso_weekday
   303     });
   304     if (i != j) { alert("ERROR"); return; }
   305     j = gregor_daycount({
   306       year: obj.year,
   307       iso_week:
   308         (obj.iso_weekyear == obj.year + 1) ? 53 :
   309         (obj.iso_weekyear == obj.year - 1) ? 0 :
   310         obj.iso_week,
   311       iso_weekday: obj.iso_weekday
   312     });
   313     if (i != j) { alert("ERROR"); return; }
   314     j = gregor_daycount({
   315       year: obj.year,
   316       us_week: obj.us_week,
   317       us_weekday: obj.us_weekday
   318     });
   319     if (i != j) { alert("ERROR"); return; }
   320   }
   321   alert("SUCCESS");
   322 }
   326 // Graphical calendar
   329 gregor_iso_weekday_css = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
   331 function gregor_sheet(args) {
   333   // setting args.today and args.selected
   334   if (args.today === undefined) {
   335     var js_date = new Date();
   336     args.today = gregor_completeDate({
   337       year:  js_date.getFullYear(),
   338       month: js_date.getMonth() + 1,
   339       day:   js_date.getDate()
   340     });
   341   } else if (args.today != null) {
   342     args.today = gregor_completeDate(args.today);
   343   }
   344   if (args.selected == "today") {
   345     args.selected = args.today;
   346   } else if (args.selected != null) {
   347     args.selected = gregor_completeDate(args.selected);
   348   }
   350   // setting args.year and args.month
   351   if (args.year == null) {
   352     if (args.selected != null) args.year = args.selected.year;
   353     else args.year = args.today.year;
   354   }
   355   if (args.month == null) {
   356     if (args.selected != null) args.month = args.selected.month;
   357     else args.month = args.today.month;
   358   }
   360   // setting first_day
   361   var first_day = gregor_completeDate({
   362     year:  args.year,
   363     month: args.month,
   364     day: 1
   365   });
   366   if (first_day == null) return;
   368   // checking args.navigation, args.week_mode and args.week_numbers
   369   if (args.navigation == null) args.navigation = "enabled";
   370   else if (
   371     args.navigation != "enabled" &&
   372     args.navigation != "disabled" &&
   373     args.navigation != "hidden"
   374   ) return;
   375   if (args.week_mode == null) args.week_mode = "iso";
   376   else if (args.week_mode != "iso" && args.week_mode != "us") return;
   377   if (
   378     args.week_numbers != null &&
   379     args.week_numbers != "left" &&
   380     args.week_numbers != "right"
   381   ) return;
   383   // checking args.month.names and args.weekday_names
   384   if (args.month_names.length != 12) return;
   385   if (args.weekday_names.length != 7) return;
   387   // calculating number of days in month
   388   var count;
   389   if (args.year < 9999 || args.month < 12) {
   390     count = gregor_daycount({
   391       year:  (args.month == 12) ? (args.year + 1) : args.year,
   392       month: (args.month == 12) ? 1 : (args.month + 1),
   393       day:   1
   394     }) - first_day.daycount;
   395   } else {
   396     // workaround for year 9999
   397     count = 31;
   398   }
   400   // locale variables for UI construction
   401   var table, row, cell, element;
   403   // take table element from args.element,
   404   // if neccessary create and store it
   405   if (args.element == null) {
   406     table = document.createElement("table");
   407     args.element = table;
   408   } else {
   409     table = args.element;
   410     while (table.firstChild) table.removeChild(table.firstChild);
   411   }
   413   // set CSS class of table according to args.week_numbers
   414   if (args.week_numbers == "left") {
   415     table.className = "gregor_sheet gregor_weeks_left";
   416   } else if (args.week_numbers == "right") {
   417     table.className = "gregor_sheet gregor_weeks_right";
   418   } else {
   419     table.className = "gregor_sheet gregor_weeks_none";
   420   }
   422   // begin of table head
   423   var thead = document.createElement("thead");
   425   // navigation
   426   if (args.navigation != "hidden") {
   428     // UI head row containing the year
   429     row = document.createElement("tr");
   430     row.className = "gregor_year_row";
   431     cell = document.createElement("th");
   432     cell.className = "gregor_year";
   433     cell.colSpan = args.week_numbers ? 8 : 7;
   434     if (args.navigation == "enabled") {
   435       element = document.createElement("a");
   436       element.className = "gregor_turn gregor_turn_left";
   437       element.style.cssFloat   = "left";
   438       element.style.styleFloat = "left";
   439       element.href = "#";
   440       element.onclick = function() {
   441         if (args.year > 1) args.year--;
   442         gregor_sheet(args);
   443         return false;
   444       }
   445       element.ondblclick = element.onclick;
   446       element.appendChild(document.createTextNode("<<"));
   447       cell.appendChild(element);
   448       element = document.createElement("a");
   449       element.className = "gregor_turn gregor_turn_right";
   450       element.style.cssFloat   = "right";
   451       element.style.styleFloat = "right";
   452       element.href = "#";
   453       element.onclick = function() {
   454         if (args.year < 9999) args.year++;
   455         gregor_sheet(args);
   456         return false;
   457       }
   458       element.ondblclick = element.onclick;
   459       element.appendChild(document.createTextNode(">>"));
   460       cell.appendChild(element);
   461     }
   462     cell.appendChild(document.createTextNode(first_day.year));
   463     row.appendChild(cell);
   464     thead.appendChild(row);
   466     // UI head row containing the month
   467     row = document.createElement("tr");
   468     row.className = "gregor_month_row";
   469     cell = document.createElement("th");
   470     cell.className = "gregor_month";
   471     cell.colSpan = args.week_numbers ? 8 : 7;
   472     if (args.navigation == "enabled") {
   473       element = document.createElement("a");
   474       element.className = "gregor_turn gregor_turn_left";
   475       element.style.cssFloat   = "left";
   476       element.style.styleFloat = "left";
   477       element.href = "#";
   478       element.onclick = function() {
   479         if (args.year > 1 || args.month > 1) {
   480           args.month--;
   481           if (args.month < 1) {
   482             args.month = 12;
   483             args.year--;
   484           }
   485         }
   486         gregor_sheet(args);
   487         return false;
   488       }
   489       element.ondblclick = element.onclick;
   490       element.appendChild(document.createTextNode("<<"));
   491       cell.appendChild(element);
   492       element = document.createElement("a");
   493       element.className = "gregor_turn gregor_turn_right";
   494       element.style.cssFloat   = "right";
   495       element.style.styleFloat = "right";
   496       element.href = "#";
   497       element.onclick = function() {
   498         if (args.year < 9999 || args.month < 12) {
   499           args.month++;
   500           if (args.month > 12) {
   501             args.month = 1;
   502             args.year++;
   503           }
   504         }
   505         gregor_sheet(args);
   506         return false;
   507       }
   508       element.ondblclick = element.onclick;
   509       element.appendChild(document.createTextNode(">>"));
   510       cell.appendChild(element);
   511     }
   512     cell.appendChild(document.createTextNode(
   513       args.month_names[first_day.month-1]
   514     ));
   515     row.appendChild(cell);
   516     thead.appendChild(row);
   518   // end of navigation
   519   }
   521   // UI weekday row
   522   row = document.createElement("tr");
   523   row.className = "gregor_weekday_row";
   524   if (args.week_numbers == "left") {
   525     cell = document.createElement("th");
   526     cell.className = "gregor_corner";
   527     row.appendChild(cell);
   528   }
   529   for (var i=0; i<7; i++) {
   530     cell = document.createElement("th");
   531     cell.className = (
   532       "gregor_weekday gregor_" +
   533       gregor_iso_weekday_css[(args.week_mode == "us") ? ((i+6)%7) : i]
   534     );
   535     cell.appendChild(document.createTextNode(args.weekday_names[i]));
   536     row.appendChild(cell);
   537   }
   538   if (args.week_numbers == "right") {
   539     cell = document.createElement("th");
   540     cell.className = "gregor_corner";
   541     row.appendChild(cell);
   542   }
   543   thead.appendChild(row);
   545   // end of table head and begin of table body
   546   table.appendChild(thead);
   547   var tbody = document.createElement("tbody");
   549   // definition of insert_week function
   550   var week = (
   551     (args.week_mode == "us") ? first_day.us_week : first_day.iso_week
   552   );
   553   insert_week = function() {
   554     cell = document.createElement("th");
   555     cell.className = "gregor_week";
   556     cell.appendChild(document.createTextNode(
   557       (week < 10) ? ("0" + week) : week)
   558     );
   559     week++;
   560     if (
   561       args.week_mode == "iso" && (
   562         (
   563           args.month == 1 && week > 52
   564         ) || (
   565           args.month == 12 && week == 53 && (
   566             first_day.iso_weekday == 0 ||
   567             first_day.iso_weekday == 5 ||
   568             first_day.iso_weekday == 6
   569           )
   570         )
   571       )
   572     ) week = 1;
   573     row.appendChild(cell);
   574   }
   576   // output data fields
   577   row = document.createElement("tr");
   578   if (args.week_numbers == "left") insert_week();
   579   var filler_count = (
   580     (args.week_mode == "us") ? first_day.us_weekday : first_day.iso_weekday
   581   );
   582   for (var i=0; i<filler_count; i++) {
   583     cell = document.createElement("td");
   584     cell.className = "gregor_empty";
   585     row.appendChild(cell);
   586   }
   587   for (var i=0; i<count; i++) {
   588     var date = gregor_completeDate({daycount: first_day.daycount + i});
   589     if (row == null) {
   590       row = document.createElement("tr");
   591       if (args.week_numbers == "left") insert_week();
   592     }
   593     cell = document.createElement("td");
   594     var cssClass = (
   595       "gregor_day gregor_" + gregor_iso_weekday_css[date.iso_weekday]
   596     );
   597     if (args.today != null && date.daycount == args.today.daycount) {
   598       cssClass += " gregor_today";
   599     }
   600     if (args.selected != null && date.daycount == args.selected.daycount) {
   601       cssClass += " gregor_selected";
   602     }
   603     cell.className = cssClass;
   604     if (args.day_callback) {
   605       args.day_callback(cell, date);
   606     } else {
   607       element = document.createElement("a");
   608       element.href = "#";
   609       var generate_onclick = function(date) {
   610         return function() {
   611           args.selected = date;
   612           gregor_sheet(args);
   613           if (args.select_callback != null) {
   614             args.select_callback(date);
   615           }
   616           return false;
   617         }
   618       }
   619       element.onclick = generate_onclick(date);
   620       element.ondblclick = element.onclick;
   621       element.appendChild(document.createTextNode(first_day.day + i));
   622       cell.appendChild(element);
   623     }
   624     row.appendChild(cell);
   625     if (row.childNodes.length == ((args.week_numbers == "left") ? 8 : 7)) {
   626       if (args.week_numbers == "right") insert_week();
   627       tbody.appendChild(row);
   628       row = null;
   629     }
   630   }
   631   if (row != null) {
   632     while (row.childNodes.length < ((args.week_numbers == "left") ? 8 : 7)) {
   633       cell = document.createElement("td");
   634       cell.className = "gregor_empty";
   635       row.appendChild(cell);
   636     }
   637     if (args.week_numbers == "right") insert_week();
   638     tbody.appendChild(row);
   639   }
   641   // end of table body
   642   table.appendChild(tbody);
   644   // return table
   645   return table;
   646 }
   650 // Rich form field support
   653 function gregor_getAbsoluteLeft(elem) {
   654  var result = 0;
   655  while (elem && elem.style.position != "static") {
   656   result += elem.offsetLeft;
   657   elem = elem.offsetParent;
   658  }
   659  return result;
   660 }
   662 function gregor_getAbsoluteTop(elem) {
   663  var result = 0;
   664  while (elem && elem.style.position != "static") {
   665   result += elem.offsetTop;
   666   elem = elem.offsetParent;
   667  }
   668  return result;
   669 }
   671 function gregor_formatDate(format, date) {
   672   if (date == null) return "";
   673   var result = format;
   674   result = result.replace(/Y+/, function(s) {
   675     if (s.length == 2) return gregor_formatInteger(date.year % 100, 2);
   676     else return gregor_formatInteger(date.year, s.length);
   677   });
   678   result = result.replace(/M+/, function(s) {
   679     return gregor_formatInteger(date.month, s.length);
   680   });
   681   result = result.replace(/D+/, function(s) {
   682     return gregor_formatInteger(date.day, s.length);
   683   });
   684   return result;
   685 }
   687 function gregor_map2digitYear(y2, maxYear) {
   688   var guess = Math.floor(maxYear / 100) * 100 + y2;
   689   if (guess <= maxYear) return guess;
   690   else return guess - 100;
   691 }
   693 function gregor_parseDate(format, string, terminated, maxYear) {
   694   var numericParts, formatParts;
   695   numericParts = string.match(/^\s*([0-9]{4})-([0-9]{2})-([0-9]{2})\s*$/);
   696   if (numericParts != null) {
   697     return gregor_completeDate({
   698       year:  numericParts[1],
   699       month: numericParts[2],
   700       day:   numericParts[3]
   701     });
   702   }
   703   numericParts = string.match(/[0-9]+/g);
   704   if (numericParts == null) return null;
   705   formatParts = format.match(/[YMD]+/g);
   706   if (numericParts.length != formatParts.length) return null;
   707   if (
   708     !terminated && (
   709       numericParts[numericParts.length-1].length <
   710       formatParts[formatParts.length-1].length
   711     )
   712   ) return null;
   713   var year, month, day;
   714   for (var i=0; i<numericParts.length; i++) {
   715     var numericPart = numericParts[i];
   716     var formatPart = formatParts[i];
   717     if (formatPart.match(/^Y+$/)) {
   718       if (numericPart.length == 2 && maxYear != null) {
   719         year = gregor_map2digitYear(parseInt(numericPart, 10), maxYear);
   720       } else if (numericPart.length > 2) {
   721         year = parseInt(numericPart, 10);
   722       }
   723     } else if (formatPart.match(/^M+$/)) {
   724       month = parseInt(numericPart, 10);
   725     } else if (formatPart.match(/^D+$/)) {
   726       day = parseInt(numericPart, 10);
   727     } else {
   728       //alert("Not implemented.");
   729       return null;
   730     }
   731   }
   732   return gregor_completeDate({year: year, month: month, day: day});
   733 }
   735 function gregor_addGui(args) {
   737   // copy argument structure
   738   var state = {};
   739   for (key in args) state[key] = args[key];
   741   // unset state.element, which should never be set anyway
   742   state.element = null;
   744   // save original values of "year" and "month" options
   745   var original_year = state.year;
   746   var original_month = state.month;
   748   // get text field element
   749   var element = document.getElementById(state.element_id);
   750   state.element_id = null;
   752   // setup state.today, state.selected and state.format options
   753   if (state.today === undefined) {
   754     var js_date = new Date();
   755     state.today = gregor_completeDate({
   756       year:  js_date.getFullYear(),
   757       month: js_date.getMonth() + 1,
   758       day:   js_date.getDate()
   759     });
   760   } else if (state.today != null) {
   761     state.today = gregor_completeDate(args.today);
   762   }
   763   if (state.selected == "today") {
   764     state.selected = state.today;
   765   } else if (args.selected != null) {
   766     state.selected = gregor_completeDate(state.selected);
   767   }
   768   if (state.format == null) state.format = "YYYY-MM-DD";
   770   // using state.future to calculate maxYear (for 2 digit year conversions)
   771   var maxYear = (state.today == null) ? null : (
   772     state.today.year +
   773     ((state.future == null) ? 12 : state.future)
   774   );
   776   // hook into state.select_callback
   777   var select_callback = state.select_callback;
   778   state.select_callback = function(date) {
   779     element.value = gregor_formatDate(state.format, date);
   780     if (select_callback) select_callback(date);
   781   };
   782   // function to parse text field and update calendar sheet state
   783   var updateSheet = function(terminated) {
   784     var date = gregor_parseDate(
   785       state.format, element.value, terminated, maxYear
   786     );
   787     if (date) {
   788       state.year = null;
   789       state.month = null;
   790     }
   791     state.selected = date;
   792     gregor_sheet(state);
   793   };
   795   // Initial synchronization
   796   if (state.selected === undefined) updateSheet(true);
   797   if (!state.relaxed)
   798     element.value = gregor_formatDate(state.format, state.selected);
   800   if (select_callback) select_callback(state.selected);
   802   // variables storing popup status
   803   var visible = false;
   804   var focus = false;
   805   var protection = false;
   807   // event handlers for text field
   808   element.onfocus = function() {
   809     focus = true;
   810     if (!visible) {
   811       state.year = original_year;
   812       state.month = original_month;
   813       gregor_sheet(state);
   814       state.element.style.position = "absolute";
   815       state.element.style.top = gregor_getAbsoluteTop(element) + element.offsetHeight;
   816       state.element.style.left = gregor_getAbsoluteLeft(element);
   817       state.element.onmousedown = function() {
   818         protection = true;
   819       };
   820       state.element.onmouseup = function() {
   821         protection = false;
   822         element.focus();
   823       };
   824       state.element.onmouseout = state.element.onmouseup;
   825       element.parentNode.appendChild(state.element);
   826       visible = true;
   827     }
   828   };
   829   element.onblur = function() {
   830     focus = false;
   831     window.setTimeout(function() {
   832       if (visible && !focus && !protection) {
   833         updateSheet(true);
   834         if(!state.relaxed)
   835           element.value = gregor_formatDate(state.format, state.selected);
   836         if (select_callback) select_callback(state.selected);
   837         state.element.parentNode.removeChild(state.element);
   838         visible = false;
   839         protection = false;
   840       }
   841     }, 1);
   842   };
   843   element.onkeyup = function() {
   844     updateSheet(false);
   845     if (select_callback) select_callback(state.selected);
   846   };
   848 }
