Revert "Fix encoding" master
authorJoey Schulze <joey@infodrom.org>
Wed, 12 Oct 2022 19:38:43 +0000 (21:38 +0200)
committerJoey Schulze <joey@infodrom.org>
Wed, 12 Oct 2022 19:38:43 +0000 (21:38 +0200)
This reverts commit a30a0eafc444ebe57bb678c269c16b10cb9e746c.

119 files changed:
.gitignore
ajax/ajax.php
ajax/ricoUpdateBoolean.php
ajax/ricoUpdateConnection.php
ajax/ricoXMLquery.php
class/actions.class.php [new file with mode: 0644]
class/autoload.class.php [new file with mode: 0644]
class/database.class.php
class/databasetable.class.php [new file with mode: 0644]
class/hallinta.class.php [new file with mode: 0644]
class/javascript.class.php [new file with mode: 0644]
class/link.class.php [new file with mode: 0644]
class/mail.class.php
class/menuitem.class.php [new file with mode: 0644]
class/scannerbase.class.php [new file with mode: 0644]
class/scannerinterface.class.php [new file with mode: 0644]
class/select.class.php [new file with mode: 0644]
class/singleton.class.php [new file with mode: 0644]
class/styles.class.php [new file with mode: 0644]
class/template.class.php [new file with mode: 0644]
config.php.template
dropdown.css [deleted file]
images/bg_header.png [new file with mode: 0644]
images/icons/csv.gif [new file with mode: 0644]
images/icons/download.gif [deleted file]
images/icons/download.png [new file with mode: 0644]
images/icons/ebook.gif [new file with mode: 0644]
images/icons/edit.png [new file with mode: 0644]
images/icons/gnumeric.png [new file with mode: 0644]
images/icons/html.png [new file with mode: 0644]
images/icons/link.png [new file with mode: 0644]
images/icons/liste.gif [new file with mode: 0644]
images/icons/menu.png [new file with mode: 0644]
index.php
init.php
lib/functions.js
lib/general.php
lib/jquery-2.1.0.min.js [new file with mode: 0644]
lib/jquery.event.ue.js [new file with mode: 0644]
lib/jquery.udraggable.js [new file with mode: 0644]
lib/login.php
lib/mask.php
lib/menu.php [deleted file]
lib/overview_utils.php
lib/php.php [new file with mode: 0644]
lib/rico/ricoCommon.js
lib/rico/ricoLiveGrid.js
lib/rico/ricoLiveGridForms.js
lib/rico/ricoXmlResponse.php
lib/rico/translations/ricoLocale_ua.js [deleted file]
lib/rico3/LICENSE [new file with mode: 0644]
lib/rico3/minsrc/rico.js [new file with mode: 0644]
lib/rico3/minsrc/ricoCalendar.js [new file with mode: 0644]
lib/rico3/minsrc/ricoGridCommon.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLiveGrid.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLiveGridAjax.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLiveGridControls.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLiveGridForms.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLiveGridMenu.js [new file with mode: 0644]
lib/rico3/minsrc/ricoLocale_en.js [new file with mode: 0644]
lib/rico3/minsrc/ricoUI.js [new file with mode: 0644]
lib/rico3/plugins/php/ricoResponse.php [new file with mode: 0644]
lib/rico3/ricoClient/css/rico.css [new file with mode: 0644]
lib/rico3/ricoClient/css/rico_icon.css [new file with mode: 0644]
lib/rico3/ricoClient/css/striping_cupertino.css [new file with mode: 0644]
lib/rico3/ricoClient/images/resize.gif [new file with mode: 0644]
lib/rico3/ricoClient/images/ricoIcons.gif [new file with mode: 0644]
lib/rico3/ricoClient/js/rico2jqu.js [new file with mode: 0644]
lib/rico3/ricoClient/js/ricoThemeroller.js [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_glass_50_3baae3_1x400.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-icons_2694e8_256x240.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-icons_2e83ff_256x240.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-icons_3d80b3_256x240.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-icons_72a7cf_256x240.png [new file with mode: 0644]
lib/rico3/ui-cupertino/images/ui-icons_ffffff_256x240.png [new file with mode: 0644]
lib/rico3/ui-cupertino/jquery-ui.css [new file with mode: 0644]
lib/rico3/ui-cupertino/jquery-ui_hallinta.css [new file with mode: 0644]
lib/ricoTableColumnDB.js
masks/README
masks/dates/send-reminder [new file with mode: 0755]
masks/documents/download.php [new file with mode: 0644]
masks/documents/files.php
masks/documents/icon/down-icon.png [new file with mode: 0644]
masks/documents/icon/scanner.png [new file with mode: 0644]
masks/documents/icon/trash-icon.png [new file with mode: 0644]
masks/documents/icon/up-icon.png [new file with mode: 0644]
masks/documents/templates/arrange.phtml [new file with mode: 0644]
masks/documents/templates/popup/close.phtml [new file with mode: 0644]
masks/documents/templates/popup/filename.phtml [new file with mode: 0644]
masks/documents/templates/scanner.phtml [new file with mode: 0644]
masks/hardware/class/hw_component.class.php [new file with mode: 0644]
masks/hardware/component.php
masks/hardware/compounds.php
masks/hardware/hardware.js [new file with mode: 0644]
masks/hardware/templates/screw/buttons.phtml [new file with mode: 0644]
masks/hardware/templates/screw/popup.phtml [new file with mode: 0644]
masks/hardware/templates/usage.phtml [new file with mode: 0644]
masks/system/group.php [new file with mode: 0644]
masks/system/menuitem.js [new file with mode: 0644]
masks/system/menuitem.php [new file with mode: 0644]
masks/system/sys_group.php [deleted file]
masks/system/sys_group_mask.php [deleted file]
masks/system/sys_mask.php [deleted file]
masks/system/sys_menu.php [deleted file]
masks/system/sys_theme_values.php
masks/system/sys_user.php [deleted file]
masks/system/user.php [new file with mode: 0644]
minimise [new file with mode: 0755]
style.css
stylesheet.css
theme.php

index 23bdc62..0f0a01a 100644 (file)
@@ -2,3 +2,6 @@ config.php
 archive/
 data/
 *~
+*.min.js
+!lib/jquery-2.1.0.min.js
+*.min.css
index 5a7a1ec..5e2cca4 100644 (file)
@@ -2,6 +2,8 @@
 
 require_once('../init.php');
 
+$mask = NULL;
+
 function fetch($mask)
 {
   global $db;
@@ -20,10 +22,8 @@ function fetch($mask)
                 implode(',', $fields),
                 $mask['table'], $_POST['id']);
 
-  $sth = $db->query($sql);
-  if ($sth === false) return false;
-
-  $row = $sth->fetch();
+  $row = $db->fetchAssoc($sql);
+  if ($row === false) return false;
 
   foreach ($mask['edit'] as $field => $info)
     if ($info['type'] == 'boolean')
@@ -75,6 +75,15 @@ function details($mask)
   return $row;
 }
 
+function load_edit($mask)
+{
+  require_once($_SESSION['sys']['basedir'].'/lib/mask.php');
+  $form = build_form($mask);
+
+  return array('content' => $form['form'],
+              'checks' => $form['checks']);
+}
+
 function format_date($value)
 {
   $d = explode('.', $value);
@@ -100,6 +109,48 @@ $upload_error = array(
                      'A PHP extension stopped the file upload.'
                      );
 
+function unique_pathname($subdir, $filename)
+{
+    $base = $_SESSION['sys']['basedir'] . 'archive';
+    if (!is_dir($base))
+       ajax_error('Archivpfad nicht vorhanden');
+
+    $base .= '/' . $subdir;
+    if (!is_dir($base))
+       ajax_error('Dokumentenpfad nicht vorhanden');
+
+    $base .= '/';
+
+    $path = date('Y');
+    if (!is_dir($base.$path))
+       if (mkdir($base.$path) === false)
+           ajax_error('Kann Verzeichnis nicht anlegen: ' . $path);
+
+    $path .= '/' . date('m');
+    if (!is_dir($base.$path))
+       if (mkdir($base.$path) === false)
+           ajax_error('Kann Verzeichnis nicht anlegen: ' . $path);
+
+    $parts = pathinfo($filename);
+
+    $parts['filename'] = $parts['filename'];
+    if (!empty($parts['extension'])) $parts['extension'] = strtolower($parts['extension']);
+    $path .= '/' . $parts['filename'];
+
+    $max = 100;
+    for ($i=0; $i < $max; $i++) {
+       $fname = $path;
+       if ($i > 0) $fname .= '('.$i.')';
+       if (!empty($parts['extension']))
+           $fname .= '.' . $parts['extension'];
+
+       if (!is_file($base.$fname))
+           return array($base, $fname);
+    }
+
+    ajax_error("Zu viele ähnliche Dateien");
+}
+
 function store_file($field, $info)
 {
   global $upload_error;
@@ -112,6 +163,10 @@ function store_file($field, $info)
     return array('error' => "Fehler beim Upload einer Datei:\n" . $upload_error[$_FILES[$field]['error']],
                 'errormsg' => 'Fehler beim Upload einer Datei');
 
+  if ($_FILES[$field]['size'] == 0)
+    return array('error' => "Datei hat keinen Inhalt\nEventuell falsch kodierte Umlaute im Dateinamen",
+                'errormsg' => 'Datei hat keinen Inhalt');
+
   if (empty($_FILES[$field]['tmp_name']))
     return array('error' => 'Keine temporäre Datei erzeugt',
                 'errormsg' => 'Keine temporäre Datei erzeugt');
@@ -120,53 +175,25 @@ function store_file($field, $info)
     return array('error' => 'Kein Pfadname konfiguriert',
                 'errormsg' => 'Kein Pfadname konfiguriert');
 
-  $parts = pathinfo($_FILES[$field]['name']);
-
-  $base = $_SESSION['sys']['basedir'] . 'archive';
-  if (!is_dir($base))
-    return array('error' => 'Archivpfad nicht vorhanden',
-                'errormsg' => 'Archivpfad nicht vorhanden');
-
-  $base .= '/' . $info['path'];
-  if (!is_dir($base))
-    return array('error' => 'Dokumentenpfad nicht vorhanden',
-                'errormsg' => 'Dokumentenpfad nicht vorhanden');
-  $base .= '/';
-
-  $path = date('Y');
-  if (!is_dir($base.$path))
-    if (mkdir($base.$path) === false)
-      return array('error' => 'Kann Verzeichnis nicht anlegen: ' . $path,
-                  'errormsg' => 'Kann Verzeichnis nicht anlegen');
-
-  $path .= '/' . date('m');
-  if (!is_dir($base.$path))
-    if (mkdir($base.$path) === false)
-      return array('error' => 'Kann Verzeichnis nicht anlegen: ' . $path,
-                  'errormsg' => 'Kann Verzeichnis nicht anlegen');
-
-  $parts['filename'] = utf8_encode($parts['filename']);
-  $path .= '/' . $parts['filename'];
-
-  $max = 100;
-  for ($i=0; $i < $max; $i++) {
-    $fname = $path;
-    if ($i > 0) $fname .= '('.$i.')';
-    if (!empty($parts['extension']))
-      $fname .= '.' . $parts['extension'];
-
-    if (!is_file($base.$fname)) {
-      if (move_uploaded_file($_FILES[$field]['tmp_name'], $base.$fname) === false)
-       return array('error' => 'Kann Datei nicht speichern ',
-                    'errormsg' => 'Kann Datei nicht speichern');
-
-      chmod($base.$fname, 0640);
-      return $fname;
-    }
+  list($base, $fname) = unique_pathname($info['path'], $_FILES[$field]['name']);
+
+  if (move_uploaded_file($_FILES[$field]['tmp_name'], $base.$fname) === false)
+      return array('error' => 'Kann Datei nicht speichern ',
+                  'errormsg' => 'Kann Datei nicht speichern');
+
+  if (filesize($base.$fname) == 0) {
+      unlink($base.$fname);
+      return array('error' => "Datei konnte nicht gespeichert werden,\nhat keinen Inhalt (Größe 0)",
+                  'errormsg' => 'Datei hat keinen Inhalt');
+  }
+
+  if ($_POST['func_use_send'] == 1) {
+      $content = utf8_decode(file_get_contents($base.$fname));
+      file_put_contents($base.$fname, $content);
   }
 
-  return array('error' => 'Kann Datei nicht anlegen',
-              'errormsg' => 'Kann Datei nicht anlegen');
+  chmod($base.$fname, 0640);
+  return $fname;
 }
 
 function save($mask)
@@ -190,6 +217,9 @@ function save($mask)
       return array('error' => sprintf('Pflichtfeld %s nicht ausgefüllt', $info['name']),
                   'errormsg' => 'Pflichtfelder nicht ausgefüllt');
 
+    if (array_key_exists('ignore', $info) && $info['ignore'])
+       continue;
+
     if ($info['type'] == 'boolean') {
       $update[] = sprintf("%s=%d", $field, $_POST[$field] == 'on'?1:0);
     } elseif ($info['type'] == 'hidden') {
@@ -262,6 +292,9 @@ function insert($mask)
       return array('error' => sprintf('Pflichtfeld %s nicht ausgefüllt', $info['name']),
                   'errormsg' => 'Pflichtfelder nicht ausgefüllt');
 
+    if (array_key_exists('ignore', $info) && $info['ignore'])
+       continue;
+
     if ($info['type'] == 'boolean') {
       $fields[] = $field;
       $values[] = $_POST[$field] == 'on'?1:0;
@@ -303,6 +336,12 @@ function insert($mask)
       $fields[] = $field;
       if (empty($_FILES[$field]) && $info['null'] === true)
         $values[] = 'NULL';
+      elseif (empty($_FILES[$field]) && array_key_exists('source', $info) && is_callable($info['source'])) {
+         $fname = call_user_func_array($info['source'], array($field, $info));
+         if (is_array($fname))
+             return $fname;
+         $values[] = $db->quote($fname);
+      }
       elseif (empty($_FILES[$field]))
         $values[] = "''";
       else {
@@ -311,13 +350,12 @@ function insert($mask)
          return $fname;
         $values[] = $db->quote($fname);
       }
-
-    } else {
+    } elseif (!array_key_exists('ignore', $info) || !$info['ignore']) {
       $fields[] = $field;
       if (empty($_POST[$field]) && isset($info['null']) && $info['null'] === true)
        $values[] = 'NULL';
       else
-       $values[] = $db->quote(utf8_encode($_POST[$field]));
+       $values[] = $db->quote($_POST[$field]);
     }
   }
 
@@ -466,28 +504,70 @@ function get_infos($mask)
 
 function custom_function($mask)
 {
-  if (!array_key_exists('callbacks',$mask))
-    return array('error' => 'Unknown callback ' . htmlspecialchars($_POST['callback']));
+    $callback = false;
+
+    if (array_key_exists('callbacks',$mask)) {
+       if (!array_key_exists($_POST['callback'],$mask['callbacks']))
+           return array('error' => 'Unknown callback ' . htmlspecialchars($_POST['callback']));
+
+       $callback = $mask['callbacks'][$_POST['callback']];
+    } else {
+       $callback = 'cb_' . $_POST['callback'];
+
+       if (!function_exists($callback))
+           $callback = false;
+    }
+
+    if (!$callback)
+       throw new Exception(sprintf("Unknown callback %s", htmlspecialchars($_POST['callback'])));
+
+    return $callback();
+}
 
-  if (!array_key_exists($_POST['callback'],$mask['callbacks']))
-    return array('error' => 'Unknown callback ' . htmlspecialchars($_POST['callback']));
+function menu($base, $source)
+{
+    $menu = new MenuItem($base);
+    $menu->setSource($source);
 
-  return $mask['callbacks'][$_POST['callback']]();
+    return array('title' => $menu->getTitle(),
+                'menu' => $menu->render());
 }
 
-function send_file($fname)
+function send_file($fname, $inline=NULL)
 {
+  if ($inline === true) {
+      $disposition = 'inline';
+  } elseif ($inline === false) {
+      $disposition = 'attachment';
+  } else {
+      if (isset($_GET['attachment']) && $_GET['attachment']) {
+         $disposition = 'attachment';
+      } else {
+         $disposition = 'inline';
+      }
+  }
+
   if (($f = fopen($fname,'r')) === false)
     return sprintf("Cannot open file %s", $fname);
 
   $basename = basename($fname);
   $filesize = filesize($fname);
+  $mtime = filemtime($fname);
   $content_type = mime_content_type($fname);
 
-  if ($content_type == 'text/plain' || $content_type == 'text/html')
-    $content_type .= '; charset=UTF-8';
+  if ($content_type == 'text/plain' || $content_type == 'text/html') {
+    $data = fread($f, 1024);
+    fseek($f, 0);
 
-  header(sprintf('Content-disposition: inline; filename="%s"', $basename));
+    if (mb_detect_encoding($data) != 'UTF-8' || mb_check_encoding($data, 'UTF-8') == false)
+      $content_type .= '; charset=ISO-8859-1';
+    else
+      $content_type .= '; charset=UTF-8';
+  }
+
+
+  header(sprintf('Last-Modified: %s', date('r', $mtime)));
+  header(sprintf('Content-disposition: %s; filename="%s"', $disposition, $basename));
   header(sprintf('Content-Length: %d', $filesize));
   header(sprintf('Content-Type: %s', $content_type));
   header("Cache-Control: ");
@@ -497,7 +577,7 @@ function send_file($fname)
   fclose($f);
 }
 
-function download_file($table,$column,$path,$id)
+function download_file($table,$column,$path,$id,$inline=NULL)
 {
   global $db;
   global $mask;
@@ -513,7 +593,7 @@ function download_file($table,$column,$path,$id)
   if (!file_exists($fname))
     return sprintf("File not found\n%s", $fname);
 
-  return send_file($fname);
+  return send_file($fname, $inline);
 }
 
 function process_file($mask)
@@ -528,38 +608,218 @@ function process_file($mask)
   exit;
 }
 
-if (empty($_REQUEST['func']))
+function process_listedit($mask, $action)
+{
+  global $db;
+
+  $list = false;
+  if ($_REQUEST['second'] === true) {
+    $secondName = substr($_REQUEST['source'],strrpos($_REQUEST['source'], '__')+2);
+    foreach ($mask['second'] as $name => $second)
+      if ($name == $secondName)
+       $list = $second;
+  } else {
+    $list = $mask['list'];
+  }
+
+  if ($list === false) {
+    error_log('Grid not found for ' . $_REQUEST['source']);
+    ajax_error('Grid not found for ' . $_REQUEST['source']);
+    exit;
+  }
+
+  $values = array();
+  foreach ($_POST as $k => $v)
+    if (substr($k,0,10) == 'listinput_')
+      $values[substr($k,10)] = $v;
+
+  switch ($action) {
+  case 'upd':
+    $update = sprintf("sys_user='%s', sys_edit=now()", $_SESSION['sys']['login']);
+    foreach ($values as $c => $v) {
+      if (!array_key_exists('edit', $list['list'][$c])) continue;
+      if (!strlen($v) && array_key_exists('isNullable', $list['list'][$c]['edit']) && $list['list'][$c]['edit']['isNullable'] === true)
+       $v = NULL;
+      $update .= sprintf(", %s = %s", $c, $db->quote($v));
+    }
+    $sql = sprintf("UPDATE %s SET %s WHERE id = %d",
+                  $list['table_edit'],
+                  $update,
+                  $_POST['_k0']);
+
+    if ($db->query($sql))
+      format_ajax(array('status' => true));
+    else
+      ajax_error('Cannot save item');
+    break;
+  }
+}
+
+function load_grid($mask)
+{
+  require_once($_SESSION['sys']['basedir'].'/lib/mask.php');
+
+  if ($_POST['name'] == 'main') {
+    if (!array_key_exists('list', $mask))
+      ajax_error('Missing grid definition');
+
+    $data = grid_details($_POST['source'], $_POST['name'], $mask);
+  } else {
+    if (!array_key_exists('second', $mask))
+      ajax_error('Missing grid definition');
+    if (!array_key_exists($_POST['name'], $mask['second']))
+      ajax_error('Missing grid definition');
+
+    $data = grid_details($_POST['source'], $_POST['name'], $mask['second'][$_POST['name']]);
+  }
+
+  return $data;
+}
+
+if (!empty($_REQUEST['_k0'])) {
+  db_connect();
+
+  foreach ($_POST as $k => $v)
+    if (substr($k,0,13) == '_action_grid_') {
+      $_REQUEST['source'] = substr($k,13);
+      $action = $v;
+    }
+
+  if (substr($_REQUEST['source'],-8) == '__second') {
+    $_REQUEST['source'] = substr($_REQUEST['source'],0,-8);
+    $_REQUEST['second'] = true;
+  } else {
+    $_REQUEST['second'] = false;
+  }
+
+  if (load_mask(Hallinta::instance()->module(),Hallinta::instance()->page()) === false) exit;
+  process_listedit($mask, $action);
   exit;
+}
+
+function render_template($mask)
+{
+    require_once($_SESSION['sys']['basedir'].'/lib/mask.php');
+
+    try {
+       $template = new Template($_POST['template']);
+    } catch (Exception $e) {
+       ajax_error(sprintf('Template %s cannot be loaded',
+                          htmlspecialchars($_POST['template'])));
+    }
+
+    $template->addData($_POST);
+    return $template->fillIn();
+}
+
+function process_function($function, $mask)
+{
+    if ($function == 'menu') {
+       $data = menu($_POST['base'], $_POST['source']);
+    } elseif ($function == 'fetch') {
+       $data = fetch($mask);
+    } elseif ($function == 'details') {
+       $data = details($mask);
+    } elseif ($function == 'edit') {
+       $data = load_edit($mask);
+    } elseif ($function == 'save') {
+       $data = save($mask);
+    } elseif ($function == 'insert') {
+       $data = insert($mask);
+    } elseif ($function == 'delete') {
+       $data = delete_or_copy($mask);
+    } elseif ($function == 'setvar') {
+       $data = set_variable($_POST['source'],$mask);
+    } elseif ($function == 'info') {
+       $data = get_infos($mask);
+    } elseif ($function == 'grid') {
+       $data = load_grid($mask);
+    } elseif ($function == 'template') {
+       $data = render_template($mask);
+    } elseif ($function == 'function') {
+       $data = custom_function($mask);
+    } else {
+       throw new Exception("Unknown function ".htmlspecialchars($function));
+    }
+
+    return $data;
+}
+
+function route_request($route, $mask)
+{
+    $data = $_POST;
+    unset($data['source']);
+    unset($data['route']);
+    unset($data['token']);
+    unset($data['id']);
+
+    list($class, $action) = explode('/', $_POST['route']);
+
+    $reflection = new ReflectionClass($class);
+
+    if (array_key_exists('id', $_POST) && strlen($_POST['id'])) {
+       $object = $reflection->newInstanceArgs([$_POST['id']]);
+
+       if (!$object->id())
+           throw new Exception(sprintf('Object %s not found', htmlspecialchars($_POST['id'])));
+    } else {
+       $object = $reflection->newInstance();
+    }
+
+    $method = 'ajax' . $action;
+    if (!method_exists($object, $method))
+       throw new Exception(sprintf("AJAX backend %s not found", htmlspecialchars($action)));
+
+    return $object->$method($data);
+}
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST' &&
+    empty($_POST) && empty($_FILES) &&
+    $_SERVER['CONTENT_LENGTH'] > 0)
+  ajax_error(sprintf('Content size probably exeeded limit of %s', ini_get('post_max_size')));
 
 if (empty($_REQUEST['source']))
-  exit;
+  ajax_error('Missing source');
 
 db_connect();
-if (load_mask($_REQUEST['source']) === false) exit;
+if ($_REQUEST['source'] != 'start' && load_mask(Hallinta::instance()->module(),Hallinta::instance()->page()) === false)
+  ajax_error('Loading source failed');
 
 $data = array('error' => 'Unknown function');
 
 if (isset($_GET['func']) && $_GET['func'] == 'file' && !empty($_GET['name']))
   $data = process_file($mask);
+elseif (empty($_SESSION['sys']['login']) || $_POST['token'] != $_SESSION['token'])
+  ajax_error('Your session has been expired');
+
+
+try {
+    if (isset($_POST['func']))
+       $data = process_function($_POST['func'], $mask);
+    elseif (isset($_POST['route']))
+       $data = route_request($_POST['route'], $mask);
+    else
+       throw new Exception("Unknown backend call");
+} catch (Exception $e) {
+    debug($e->getMessage());
+    debug($e->getTraceAsString());
+    ajax_error($e->getMessage());
+} catch (Error $e) {
+    debug($e->getMessage());
+    debug($e->getTraceAsString());
+    ajax_error($e->getMessage());
+}
+
+if (is_null($data))
+    $data = [];
+elseif (is_scalar($data))
+    $data = ['data' => $data];
 
-if ($_POST['func'] == 'fetch') {
-  $data = fetch($mask);
-} elseif ($_POST['func'] == 'details') {
-  $data = details($mask);
-} elseif ($_POST['func'] == 'save') {
-  $data = save($mask);
-} elseif ($_POST['func'] == 'insert') {
-  $data = insert($mask);
-} elseif ($_POST['func'] == 'delete') {
-  $data = delete_or_copy($mask);
-} elseif ($_POST['func'] == 'setvar') {
-  $data = set_variable($_POST['source'],$mask);
-} elseif ($_POST['func'] == 'info') {
-  $data = get_infos($mask);
-} elseif ($_POST['func'] == 'function') {
-  $data = custom_function($mask);
+if (is_array($data)) {
+    if (isset($_POST['func']) && !array_key_exists('func', $data))
+       $data['func'] = $_POST['func'];
+    elseif (isset($_POST['route']) && !array_key_exists('route', $data))
+       $data['route'] = $_POST['route'];
 }
 
 format_ajax($data);
-
-?>
index 2d1d12b..6924fa6 100644 (file)
@@ -55,8 +55,9 @@ function refcol($mask, $column) {
 if (empty($_POST['table']))
   exit;
 
+$hallinta = Hallinta::instance();
 db_connect();
-if (load_mask(substr($_POST['table'],5)) === false) exit;
+if (load_mask($hallinta->module(),$hallinta->page()) === false) exit;
 
 $fields = array_keys($mask['list']);
 $column = $fields[$_POST['column']];
@@ -74,4 +75,3 @@ $sth = $db->query($sql);
 if ($sth === false)
   error_log($sql . ': ' . db_error());
 
-?>
index 821cd00..f3e864e 100644 (file)
@@ -41,11 +41,19 @@ function build_query($details,$mask)
     $sql = sprintf("DELETE FROM %s WHERE %s = %d AND %s IN (SELECT %s FROM %s%s%s)",
                   $details['table'],
                   $details['basecol'], $details['baseval'],
-                  $details['refcol'], $details['refid'], 
+                  $details['refcol'], $details['refid'],
                   $details['reftable'],
                   array_key_exists('join', $mask) ? ' JOIN ' . join(' JOIN ', $mask['join']) : '',
                   strlen($where) ? ' WHERE ' . $where : '');
-  else
+  else {
+    $sql = sprintf("DELETE FROM %s WHERE %s = %d AND %s IN (SELECT %s FROM %s%s%s)",
+                  $details['table'],
+                  $details['basecol'], $details['baseval'],
+                  $details['refcol'], $details['refid'],
+                  $details['reftable'],
+                  array_key_exists('join', $mask) ? ' JOIN ' . join(' JOIN ', $mask['join']) : '',
+                  strlen($where) ? ' WHERE ' . $where : '');
+    $db->execute($sql);
     $sql = sprintf("INSERT INTO %s (%s,%s,sys_user,sys_edit) SELECT %s,%s,%s,now() FROM %s%s " .
                   "LEFT JOIN %s ON %s.%s = 1 AND %s.%s = %s.id WHERE %s.id IS NULL%s",
                   $details['table'],
@@ -60,6 +68,7 @@ function build_query($details,$mask)
                   $details['reftable'],
                   $details['table'],
                   strlen($where) ? ' AND ' . $where : '');
+  }
 
   return $sql;
 }
@@ -67,24 +76,36 @@ function build_query($details,$mask)
 if (empty($_POST['table']))
   exit;
 
+$hallinta = Hallinta::instance();
 db_connect();
-if (load_mask(substr($_POST['table'],5)) === false) exit;
+if (load_mask($hallinta->module(),$hallinta->page()) === false) exit;
+
+if ($hallinta->isSecond()) {
+    if (!array_key_exists('second', $mask) ||
+       !array_key_exists($hallinta->second(), $mask['second']))
+       exit;
+    $list = $mask['second'][$hallinta->second()]['list'];
+} else {
+    $list = $mask['list'];
+}
 
-$fields = array_keys($mask['list']);
+$fields = array_keys($list);
 $field = $fields[$_POST['column']];
 
 if (empty($field)) exit;
 
-if (!is_array($mask['list'][$field]['update'])) return;
+if (!is_array($list[$field]['update'])) exit;
+
+if (!array_key_exists('baseval', $list[$field]['update']))
+    $list[$field]['update']['baseval'] = $_POST['main_id'];
 
 if (isset($_POST['reference']))
-  $sql = build_simple_query($mask['list'][$field]['update']);
+  $sql = build_simple_query($list[$field]['update']);
 else 
-  $sql = build_query($mask['list'][$field]['update'],$mask);
+  $sql = build_query($list[$field]['update'],$mask);
 
 $sth = $db->query($sql);
 
 if ($sth === false)
   error_log($sql . ': ' . db_error());
 
-?>
index da8fd93..07551bb 100644 (file)
@@ -6,71 +6,39 @@ header("Cache-Control: no-cache");
 header("Pragma: no-cache");\r
 header("Expires: ".gmdate("D, d M Y H:i:s",time()+(-1*60))." GMT");\r
 header("Content-type: text/xml");\r
-echo "<?xml version='1.0' encoding='UTF-8'?".">\n";\r
 \r
 require_once('../lib/dbase.php');\r
 require_once('../lib/dbClass.php');\r
-require_once('../lib/rico/ricoXmlResponse.php');\r
+require_once('../lib/rico3/plugins/php/ricoResponse.php');\r
 \r
 $id=isset($_GET["id"]) ? $_GET["id"] : "";\r
-$offset=isset($_GET["offset"]) ? $_GET["offset"] : "0";\r
-$size=isset($_GET["page_size"]) ? $_GET["page_size"] : "";\r
-$total=isset($_GET["get_total"]) ? strtolower($_GET["get_total"]) : "false";\r
-$distinct=isset($_GET["distinct"]) ? $_GET["distinct"] : "";\r
+$filters=isset($_SESSION[$id . ".filters"]) ? $_SESSION[$id . ".filters"] : array();\r
+$oXmlResp= new ricoXmlResponse();\r
+$errmsg='';\r
+$query='';\r
 \r
-echo "\n<ajax-response><response type='object' id='".$id."_updater'>";\r
-if (empty($id)) {\r
-  ErrorResponse("No ID provided!");\r
-} elseif ($distinct=="" && !is_numeric($offset)) {\r
-  ErrorResponse("Invalid offset!");\r
-} elseif ($distinct=="" && !is_numeric($size)) {\r
-  ErrorResponse("Invalid size!");\r
-} elseif ($distinct!="" && !is_numeric($distinct)) {\r
-  ErrorResponse("Invalid distinct parameter!");\r
-} elseif (!isset($_SESSION[$id])) {\r
-  ErrorResponse("Your connection with the server was idle for too long and timed out. Please refresh this page and try again.");\r
+if (!isset($_SESSION[$id])) {\r
+  $errmsg="Your connection with the server was idle for too long and timed out. Please refresh this page and try again.";\r
 } elseif (!OpenDB()) {\r
-  ErrorResponse(htmlspecialchars($oDB->LastErrorMsg));\r
+  $errmsg=$oDB->LastErrorMsg;\r
 } else {\r
-  if (empty($_GET['second_id'])) {\r
-    $fname = substr($id,5);\r
-    load_mask($fname);\r
-    grid_sql($fname, $mask);\r
-  }\r
-\r
-  $filters=isset($_SESSION[$id . ".filters"]) ? $_SESSION[$id . ".filters"] : array();\r
-  $oDB->DisplayErrors=false;\r
-  $oDB->ErrMsgFmt="MULTILINE";\r
-  $oXmlResp= new ricoXmlResponse();\r
+  $query=$_SESSION[$id];\r
+  if (isset($_SESSION[$id . ".filters"])) $filters=$_SESSION[$id . ".filters"];\r
+  $oXmlResp->SetDbConn($oDB);\r
   $oXmlResp->sendDebugMsgs=true;\r
-  $oXmlResp->convertCharSet=false;  // Database is already in UTF-8\r
-\r
-  if (isset($_GET['second_id']))\r
-    $query = str_replace('{id}', $_GET['second_id'], $_SESSION[$id]);\r
-  else\r
-    $query = $_SESSION[$id];\r
-\r
-  if ($distinct=="") {\r
-    $oXmlResp->Query2xml($query, intval($offset), intval($size), $total!="false", $filters);\r
-  } else {\r
-    if (isset($_SESSION[$id.'_distinct_'.$distinct]))\r
-      $oXmlResp->Query2xmlDistinct($query, intval($distinct), -1, $filters, $_SESSION[$id.'_distinct_'.$distinct]);\r
-    else\r
-      $oXmlResp->Query2xmlDistinct($query, intval($distinct), -1, $filters);\r
-  }\r
-  if (!empty($oDB->LastErrorMsg)) {\r
-    echo "\n<error>";\r
-    echo "\n".htmlspecialchars($oDB->LastErrorMsg);\r
-    echo "\n</error>";\r
-  }\r
-  $oXmlResp=NULL;\r
-  # CloseApp();\r
+  $oXmlResp->convertCharSet=false;\r
 }\r
-echo "\n</response></ajax-response>";\r
 \r
+if (isset($_GET['second_id']))\r
+  $query = str_replace('{id}', $_GET['second_id'], $_SESSION[$id]);\r
+else\r
+  $query = $_SESSION[$id];\r
 \r
-function ErrorResponse($msg) {\r
-  echo "\n<rows update_ui='false' /><error>" . $msg . "</error>";\r
-}\r
+$distinctquery = false;\r
+$distinct = isset($_GET["distinct"]) ? $_GET["distinct"] : false;\r
+if ($distinct !== false && isset($_SESSION[$id.'_distinct_'.$distinct]))\r
+  $distinctquery = $_SESSION[$id.'_distinct_'.$distinct];\r
 \r
-?>\r
+$oXmlResp->SetGridDefinition(grid_definition($id));\r
+$oXmlResp->ProcessQuery($id, $query, $filters, $errmsg, $distinctquery);\r
+$oXmlResp=NULL;\r
diff --git a/class/actions.class.php b/class/actions.class.php
new file mode 100644 (file)
index 0000000..3e023ef
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+class Actions extends Singleton {
+  protected static $instance = false;
+  private $actions = array();
+  private $selects = array();
+
+  public function addLink(Link $info)
+  {
+    $this->actions[] = $info;
+  }
+
+  public function addSelect(Select $info)
+  {
+    $this->selects[] = $info;
+  }
+
+  public function toString()
+  {
+    $ret = '';
+    $code = array();
+    foreach (array_reverse($this->selects) as $item) {
+      $code[] = sprintf('<span class="item">%s</span>', $item->toString());
+    }
+    foreach (array_reverse($this->actions) as $item) {
+      $code[] = sprintf('<span class="item">%s</span>', $item->toString());
+    }
+    $ret .= implode('', $code);
+
+    return $ret;
+  }
+
+}
diff --git a/class/autoload.class.php b/class/autoload.class.php
new file mode 100644 (file)
index 0000000..4c7a799
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+class Autoload {
+    protected static $instance;
+    private $basedir;
+
+    public static function get()
+    {
+       if (is_null(static::$instance))
+           static::$instance = new static();
+
+       return static::$instance;
+    }
+
+    private function __construct()
+    {
+       $this->basedir = dirname(__DIR__);
+    }
+
+    public function loadClassModule($class)
+    {
+       $module = false;
+
+       if (class_exists('Hallinta', false)) {
+           $module = Hallinta::instance()->module();
+       } elseif (!empty($_REQUEST['source']) && strpos($_REQUEST['source'], '__')) {
+           $name = sanitise_filename($_REQUEST['source']);
+           list($module,$fname) = explode('__', $name);
+       }
+
+       if ($module) {
+           $path = sprintf("%s/%s/%s/class/%s.class.php", $this->basedir,
+                           HALLINTA_MODULEDIR, $module, strtolower($class));
+           if (file_exists($path))
+               require_once($path);
+       }
+    }
+
+    public function loadClass($class)
+    {
+       $path = sprintf("%s/class/%s.class.php", $this->basedir,
+                       strtolower($class));
+       if (file_exists($path))
+           require_once($path);
+    }
+}
+
+spl_autoload_register([Autoload::get(), 'loadClassModule']);
+spl_autoload_register([Autoload::get(), 'loadClass']);
index 47be12d..6ae81a6 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 class Database {
+  private static $instance = NULL;
   private $db;
   private $error_log = false;
   private $error_mail = false;
@@ -14,6 +15,22 @@ class Database {
     #    $this->db->query("SET CHARACTER SET 'utf8'");
     #    $this->db->query("SET collation_connection = 'utf8_general_ci'");
     #    $this->db->query("SET lc_time_names = 'de_DE'");
+
+    static::registerFirst($this);
+  }
+
+  public static function get()
+  {
+      if (is_null(static::$instance))
+         throw new Exception("No database object available");
+
+      return static::$instance;
+  }
+
+  private static function registerFirst($db)
+  {
+      if (is_null(static::$instance))
+         static::$instance = $db;
   }
 
   public function enableErrorLog()
@@ -71,6 +88,12 @@ class Database {
 
       $mail->send($body);
     }
+    return $sth;
+  }
+
+  public function errorInfo()
+  {
+    return $this->db->errorInfo();
   }
 
   public function quote($string)
@@ -93,7 +116,7 @@ class Database {
     $this->logQuery($sql);
     $sth = $this->db->query($sql);
 
-    if ($sth === false) $this->handleError($sth,$sql);
+    if ($sth === false) return $this->handleError($sth,$sql);
 
     if (preg_match('/INSERT\s+INTO\s+(\S+)\s+/i', $sql, $matches))
       $this->lastInsertTable = $matches[1];
@@ -150,5 +173,3 @@ class Database {
   }
 
 }
-
-?>
diff --git a/class/databasetable.class.php b/class/databasetable.class.php
new file mode 100644 (file)
index 0000000..4bd9129
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+abstract class DatabaseTable {
+    protected $db;
+    protected $idcolumn = 'id';
+    protected $username = 'class';
+    protected $table;
+    protected $data;
+    protected $id;
+
+    public function __construct($table, $id=false, $column=false)
+    {
+       global $db;
+
+        $this->db = $db;
+        $this->table = $table;
+
+        if (isset($_SESSION['sys']['login']))
+            $this->username = $_SESSION['sys']['login'];
+
+        if ($id) {
+           if ($column)
+               $this->loadByColumn($id, $column);
+           else
+               $this->load($id);
+       }
+    }
+
+    protected function postLoad() {}
+
+    protected function load($id)
+    {   
+        $sql = sprintf("SELECT * FROM %s WHERE %s = %d LIMIT 1",
+                       $this->table, $this->idcolumn, $id);
+        $this->data = $this->db->fetchObject($sql);
+        if ($this->data) {
+            $idcolumn = $this->idcolumn;
+            $this->id = $this->data->$idcolumn;
+            $this->postLoad();
+            return true;
+        }
+        return false;
+    }
+
+    protected function loadByColumn($id,$column)
+    {   
+        $sql = sprintf("SELECT * FROM %s WHERE `%s` = %s",
+                       $this->table, $column, $this->db->quote($id));
+        $this->data = $this->db->fetchObject($sql);
+        if ($this->data) {
+            $idcolumn = $this->idcolumn;
+            $this->id = $this->data->$idcolumn;
+            $this->postLoad();
+        }
+    }
+
+    public function id()
+    {
+        return $this->id;
+    }
+
+    public function get($name)
+    {   
+       if (array_key_exists($name, $this->data))
+           return $this->data->$name;
+
+       return NULL;
+    }
+}
\ No newline at end of file
diff --git a/class/hallinta.class.php b/class/hallinta.class.php
new file mode 100644 (file)
index 0000000..1ff036d
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+
+class Hallinta extends Singleton {
+  protected static $instance = false;
+    protected $db = null;
+    protected $module = null;
+    protected $page = null;
+    protected $urlbase = null;
+    protected $second = null;
+    protected $title = null;
+
+    protected function ___construct()
+    {
+       global $db;
+
+       $this->db = $db;
+
+       if (empty($_SESSION['sys']['baseurl']) && substr($_SERVER['SCRIPT_FILENAME'],-10) == '/index.php') {
+           $this->urlbase = substr(substr($_SERVER['SCRIPT_FILENAME'],0,-9), strlen($_SERVER['DOCUMENT_ROOT']));
+       } else {
+           if (substr($_SESSION['sys']['baseurl'],0,1) != '/')
+               $_SESSION['sys']['baseurl'] = '/' . $_SESSION['sys']['baseurl'];
+           if (substr($_SESSION['sys']['baseurl'],-1) != '/')
+               $_SESSION['sys']['baseurl'] .= '/';
+           $this->urlbase = $_SESSION['sys']['baseurl'];
+       }
+
+       if (!empty($_REQUEST['table']) && empty($_REQUEST['source']) && substr($_REQUEST['table'],0,5) == 'grid_') {
+           $_REQUEST['source'] = substr($_POST['table'],5);
+       }
+
+       if (!empty($_REQUEST['source'])) {
+           $parts = explode('__', $_REQUEST['source']);
+           if (count($parts) > 1) {
+               $this->module = $parts[0];
+               $this->page = $parts[1];
+               if (count($parts) == 4) {
+                   $this->second = $parts[2];
+               }
+           }
+       } elseif (!empty($_GET['mask'])) {
+           list($this->module, $this->page) = explode('__', $_GET['mask']);
+       } elseif (basename($_SERVER['SCRIPT_FILENAME']) == 'ricoXMLquery.php' && !empty($_GET['id'])) {
+           list($this->module, $this->page) = explode('__', substr($_GET['id'],5));
+       } elseif (!empty($_SERVER['REQUEST_URI']) && isset($_SESSION['sys']['baseurl'])) {
+           $uri = rtrim(substr($_SERVER['REQUEST_URI'], strlen($_SESSION['sys']['baseurl'])), '/');
+           $parts = explode('/', $uri);
+           if (count($parts) == 2) {
+               $this->module = $parts[0];
+               $this->page = $parts[1];
+           }
+       }
+    }
+
+    public function basedir()
+    {
+       if (isset($_SESSION['sys']['basedir']))
+           return $_SESSION['sys']['basedir'];
+
+       return $_SERVER['DOCUMENT_ROOT'] . $this->urlbase();
+    }
+
+    public function urlbase()
+    {
+       return $this->urlbase;
+    }
+
+    public function module()
+    {
+       return $this->module;
+    }
+
+    public function page()
+    {
+       return $this->page;
+    }
+
+    public function second()
+    {
+       return $this->second;
+    }
+
+    public function isSecond()
+    {
+       return !is_null($this->second);
+    }
+
+    public function isMobile()
+    {
+       return strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false;
+    }
+
+    public function parentMenu()
+    {
+       $sql = sprintf("SELECT parent FROM sys_menuitem WHERE module = %s AND page = %s",
+                      $this->db->quote($this->module),
+                      $this->db->quote($this->page));
+       $parent = $this->db->fetchValue($sql);
+
+       if ($parent === 0) return '';
+
+       return $parent;
+    }
+
+    public function setTitle($title)
+    {
+       $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+       if (is_null($this->title) && isset($_GET['login']) && $_GET['login'] == 'true')
+           return '';
+
+       return $this->title;
+    }
+}
diff --git a/class/javascript.class.php b/class/javascript.class.php
new file mode 100644 (file)
index 0000000..025a368
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+class JavaScript extends Singleton {
+  protected static $instance = false;
+  protected $code = array();
+  protected $onload = array();
+  protected $files = array();
+
+  public function add($code)
+  {
+    $this->code[] = $code;
+  }
+
+  public function onLoad($code)
+  {
+    $this->onload[] = $code;
+  }
+
+  public function file($path)
+  {
+    $hallinta = Hallinta::instance();
+
+    if (strpos($path, '/') === false)
+       $path = sprintf('%s/%s/%s', HALLINTA_MODULEDIR, $hallinta->module(), $path);
+
+    $info = pathinfo($path);
+
+    $minfile = sprintf('%s%s/%s.min.%s', $hallinta->basedir(), $info['dirname'], $info['filename'], $info['extension']);
+    if (file_exists($minfile)) {
+      $origpath = $hallinta->basedir().$path;
+      if (filemtime($minfile) > filemtime($origpath))
+       $path = sprintf('%s/%s.min.%s', $info['dirname'], $info['filename'], $info['extension']);
+    }
+
+    $this->files[$path] = true;
+  }
+
+  public function toString()
+  {
+    $ret = '';
+    foreach (array_keys($this->files) as $file)
+      $ret .= sprintf('<script type="text/javascript" src="%s%s"></script>'."\n", Hallinta::instance()->urlbase(), $file);
+
+    if (count($this->code) || count($this->onload)) {
+      $ret .= '<script type="text/javascript">'."\n";
+
+      if (count($this->code))
+       $ret .= implode("\n", $this->code) . "\n";
+
+      if (count($this->onload)) {
+       $ret .= "\$(function(){\n";
+       $ret .= '  ' . str_replace("\n", "\n  ", implode("\n", $this->onload)) . "\n";
+       $ret .= "});\n";
+      }
+      $ret .= "</script>\n";
+    }
+
+    return $ret;
+  }
+
+}
diff --git a/class/link.class.php b/class/link.class.php
new file mode 100644 (file)
index 0000000..e242810
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+class Link {
+  protected $id = false;
+  protected $icon;
+  protected $title;
+  protected $link = false;
+  protected $function = false;
+  protected $anchor = false;
+
+  public function __construct(array $info)
+  {
+    if (!array_key_exists('icon', $info))
+      throw new Exception('Property icon missing');
+    else $this->icon = $info['icon'];
+
+    if (!array_key_exists('title', $info))
+      throw new Exception('Property title missing');
+    else $this->title = $info['title'];
+
+    if (!array_key_exists('link', $info) && !array_key_exists('function', $info))
+      throw new Exception('Property link and function missing');
+
+    if (array_key_exists('id', $info)) $this->id = $info['id'];
+    if (array_key_exists('link', $info)) $this->link = $info['link'];
+    if (array_key_exists('function', $info)) $this->function = $info['function'];
+    if (array_key_exists('anchor', $info)) $this->anchor = $info['anchor'];
+  }
+
+  public function toString()
+  {
+    if ($this->anchor) {
+      $imgattr = array('border="0"');
+      $attr = array();
+      if ($this->id) $attr[] = sprintf('id="%s"', $this->id);
+      if ($this->title) $attr[] = sprintf('title="%s"', $this->title);
+      if ($this->icon) $imgattr[] = sprintf('src="%s"', $this->icon);
+      if ($this->function) {
+       $attr[] = 'href="#"';
+       $attr[] = sprintf('onclick="return %s(event)"', $this->function);
+      } elseif ($this->link)
+         $attr[] = sprintf('href="%s"', $this->link);
+      $code = sprintf('<a %s><img %s></a>', implode(' ', $attr),  implode(' ', $imgattr));
+    } else {
+      $attr = array('border="0"');
+      if ($this->id) $attr[] = sprintf('id="%s"', $this->id);
+      if ($this->title) $attr[] = sprintf('title="%s"', $this->title);
+      if ($this->icon) $attr[] = sprintf('src="%s"', $this->icon);
+      if ($this->function)
+       $attr[] = sprintf('onclick="return %s(event)"', $this->function);
+      elseif ($this->link)
+       $attr[] = sprintf("onclick=\"function(){ window.href.location = '%s'; }\"", $this->link);
+      $code = sprintf('<img %s>', implode(' ', $attr));
+    }
+    return $code;
+  }
+}
index 855bf18..06750ee 100644 (file)
@@ -3,6 +3,8 @@
 class Mail {
   protected $header = array();
   protected $env_from = false;
+  protected $attachments = array();
+  protected $CRLF = "\r\n";
 
   public function set($name, $value)
   {
@@ -14,7 +16,51 @@ class Mail {
     $this->env_from = $value;
   }
 
-  public function send($body)
+  protected function contentType($path)
+  {
+    if (($fh = popen('/usr/bin/file -i -b ' . escapeshellarg(realpath($path)), 'r')) !== false) {
+      $type = fread($fh, 1024);
+      fclose($fh);
+      $parts = explode(';', $type);
+      return trim($parts[0]);
+    } else
+      return mime_content_type($file);
+  }
+
+  public function attach($content, $type=false, $first=false)
+  {
+    $part = new stdClass();
+
+    if ($type === false) {
+      if (file_exists($content)) {
+       $part->content = chunk_split(base64_encode(file_get_contents($content)));
+       $part->header = 'Content-Type: ' . $this->contentType($content) . '; name="'.basename($content).'"'. $this->CRLF .
+         'Content-Transfer-Encoding: base64' . $this->CRLF .
+         'Content-Disposition: attachment';
+      }
+    } elseif (substr($type, 0, 10) == 'text/plain' ||
+             substr($type, 0, 9) == 'text/html') {
+      if (strpos($type, 'charset=') === false)
+       $type .= '; charset="UTF-8"';
+      $part->content = $content;
+      $part->header = 'Content-Type: ' . $type . $this->CRLF .
+       'Content-Transfer-Encoding: 8bit';
+    } else {
+      $part->content = chunk_split(base64_encode($content));
+      $part->header = 'Content-Type: ' . $type . $this->CRLF .
+       'Content-Transfer-Encoding: base64' . $this->CRLF .
+       'Content-Disposition: attachment';
+    }
+
+    if (!empty($part->content)) {
+      if ($first)
+       array_unshift($this->attachments, $part);
+      else
+       $this->attachments[] = $part;
+    }
+  }
+
+  public function send($body=false)
   {
     if (!array_key_exists('From', $this->header))
       throw new Exception('No sender given.');
@@ -22,27 +68,68 @@ class Mail {
       throw new Exception('No recipient given.');
     if (!array_key_exists('Subject', $this->header))
       throw new Exception('No subject given.');
-    if (empty($body))
-      throw new Exception('Mail body empty.');
+    if (!count($this->attachments) && empty($body))
+      throw new Exception('Mail body empty and no attachments.');
 
-    if (!array_key_exists('Content-Type', $this->header))
-      $this->set('Content-Type', 'text/plain; charset=UTF-8');
-    if (!array_key_exists('Content-Disposition', $this->header))
-      $this->set('Content-Disposition', 'inline');
-    if (!array_key_exists('Content-Transfer-Encoding', $this->header))
-      $this->set('Content-Transfer-Encoding', '8bit');
+    if (count($this->attachments)) {
+      $boundary = md5(uniqid(time()));
+      $this->header['MIME-Version'] = array('1.0');
+      $this->header['Content-Type'] = array('multipart/mixed; boundary="'.$boundary.'"');
+      if ($body)
+       $this->attach($body, substr($body,0,1) == '<' ? 'text/html' : 'text/plain', true);
+    } else {
+      if (!array_key_exists('Content-Type', $this->header)) {
+       if ($body && substr($body,0,1) == '<')
+         $this->set('Content-Type', 'text/html; charset=UTF-8');
+       else
+         $this->set('Content-Type', 'text/plain; charset=UTF-8');
+      }
+      if (!array_key_exists('Content-Disposition', $this->header))
+       $this->set('Content-Disposition', 'inline');
+      if (!array_key_exists('Content-Transfer-Encoding', $this->header))
+       $this->set('Content-Transfer-Encoding', '8bit');
+    }
 
     $header = '';
+    $to = implode(', ', $this->header['To']);
+    if (defined('MAIL_DISABLED') && MAIL_DISABLED == true) {
+      $header .= 'Orig-To: ' . $to . $this->CRLF;
+      $to = MAIL_DISABLED;
+
+      if (array_key_exists('Cc', $this->header)) {
+       $this->header['Orig-Cc'] = $this->header['Cc'];
+       unset($this->header['Cc']);
+      }
+      if (array_key_exists('Bcc', $this->header)) {
+       $this->header['Orig-Bcc'] = $this->header['Bcc'];
+       unset($this->header['Bcc']);
+      }
+    }
     foreach ($this->header as $name => $values) {
       if ($name == 'To' || $name == 'Subject')
        continue;
       else
-       $header .= $name . ': ' . implode(', ', $values) . "\r\n";
+       $header .= $name . ': ' . implode(', ', $values) . $this->CRLF;
     }
 
     $opts = '-t';
     $opts .= strlen($this->env_from) ? ' -f '.$this->env_from : '';
-    $result = mail(implode(',',$this->header['To']), $this->header['Subject'][0], $body, $header, $opts);
+
+    $header = substr($header,0,-strlen($this->CRLF));
+    if (count($this->attachments)) {
+      $header .= $this->CRLF . "This is a MIME encoded message." . $this->CRLF;
+
+      foreach ($this->attachments as $part) {
+       $header .= "--$boundary" . $this->CRLF;
+       $header .= $part->header . $this->CRLF;
+       $header .= $this->CRLF . $part->content . $this->CRLF;
+      }
+      $header .= "--$boundary--";
+
+      $result = mail($to, $this->header['Subject'][0], '', $header, $opts);
+    } else {
+      $result = mail($to, $this->header['Subject'][0], $body, $header, $opts);
+    }
 
     return $result;
   }
diff --git a/class/menuitem.class.php b/class/menuitem.class.php
new file mode 100644 (file)
index 0000000..452c16b
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+
+class MenuItem {
+    protected $db = null;
+    protected $parent = false;
+    protected $source = false;
+
+    public function __construct($parent=false)
+    {
+       global $db;
+       $this->db = $db;
+       $this->parent = $parent;
+    }
+
+    public function setSource($source)
+    {
+       $this->source = $source;
+    }
+
+    protected function getItems()
+    {
+       $sql = "
+        SELECT DISTINCT sys_menuitem.id, title, tooltip,
+          module, page,
+          priority,
+          module IS NULL AND page IS NULL AS submenu
+        FROM sys_menuitem
+       JOIN sys_group_menuitem ON sys_menuitem.id = sys_menuitem_id
+       JOIN sys_group ON sys_group.id = sys_group_menuitem.sys_group_id
+       JOIN sys_group_user ON sys_group.id = sys_group_user.sys_group_id
+        WHERE parent = %d AND shadow = 0 AND sys_user_id = %d
+        ORDER BY priority
+        ";
+       $sql = sprintf($sql, $this->parent, $_SESSION['sys']['uid']);
+
+       return $this->db->fetchObjectList($sql);
+    }
+
+    public function getTitle()
+    {
+       if (empty($this->parent))
+           return 'Hauptmenü';
+
+       $sql = sprintf("SELECT title FROM sys_menuitem WHERE id = %d", $this->parent);
+       return $this->db->fetchValue($sql);
+    }
+
+    protected function getParent()
+    {
+       $sql = sprintf("SELECT parent FROM sys_menuitem WHERE id = %d", $this->parent);
+
+       return $this->db->fetchValue($sql);
+    }
+
+    public function render()
+    {
+       $result = array();
+       if (empty($this->parent))
+           $result[] = sprintf('<li%s><a href="%s" title="Zurück zur Hauptseite">Start</a></li>',
+                               $this->source == 'start' ? ' class="current"' : '',
+                               Hallinta::instance()->urlbase());
+
+       $rows = $this->getItems();
+
+       foreach ($rows as $row) {
+           if ($row->submenu) {
+               $result[] = sprintf('<li><a href="#" onclick="return display_menu(event,%d)" title="%s">%s...</a></li>',
+                                   $row->id, $row->tooltip, $row->title);
+           } else {
+               if (defined('REWRITE_URLS') && REWRITE_URLS)
+                   $url = sprintf('%s%s/%s', Hallinta::instance()->urlbase(), $row->module, $row->page);
+               else
+                   $url = sprintf('./?mask=%s__%s', $row->module, $row->page);
+               $result[] = sprintf('<li%s><a href="%s" title="%s">%s</a></li>',
+                                   $this->source == ($row->module.'__'.$row->page) ? ' class="current"' : '',
+                                   $url,
+                                   $row->tooltip,
+                                   $row->title);
+           }
+       }
+
+
+       if (empty($this->parent))
+           $result[] = sprintf('<li><a href="%s?logout=true" title="Abmelden">Logout</a></li>', Hallinta::instance()->urlbase());
+       else
+           $result[] = sprintf('<li><a href="#" onclick="return display_menu(event,\'%s\')" title="Zurück">Zurück</a></li>',
+                               $this->getParent());
+
+       if (!count($result)) return '';
+
+       return '<ul class="menu">' . implode($result) . '</ul>';
+    }
+
+    public function getMenus()
+    {
+       $sql = "SELECT id,title AS text FROM sys_menuitem WHERE module IS NULL AND page IS NULL";
+
+       return $this->db->fetchAssocList($sql);
+    }
+
+    public function getModules()
+    {
+       $list = [];
+       $dir = Hallinta::instance()->basedir() . HALLINTA_MODULEDIR;
+
+       foreach (new DirectoryIterator($dir) as $fileInfo) {
+           if ($fileInfo->isDot()) continue;
+           if (!$fileInfo->isDir()) continue;
+           if (file_exists($dir . '/' . $fileInfo->getFileName() . '/' . '.moduleignore')) continue;
+           $list[] = ['id' => $fileInfo->getFileName(), 'text' => $fileInfo->getFileName()];
+       }
+
+       usort($list, function($a, $b){ return strcmp($a['text'], $b['text']); });
+
+       return $list;
+    }
+
+    public function getPages($module)
+    {
+       $list = [];
+       $module = str_replace('/', '', $module);
+
+       $dir = Hallinta::instance()->basedir() . HALLINTA_MODULEDIR . '/' . $module;
+
+       if (!is_dir($dir)) return [];
+       foreach (new DirectoryIterator($dir) as $fileInfo) {
+           if ($fileInfo->isDot()) continue;
+           if (!$fileInfo->isFile()) continue;
+           if (substr($fileInfo->getFileName(), -4) != '.php') continue;
+
+           $page = substr($fileInfo->getFileName(),0,-4);
+           $list[] = ['id' => $page, 'text' => $page];
+       }
+
+       usort($list, function($a, $b){ return strcmp($a['text'], $b['text']); });
+
+       return $list;
+    }
+
+    public function hasPermission()
+    {
+       $hallinta = Hallinta::instance();
+
+       $sql = <<<EOS
+           SELECT count(*)
+           FROM sys_menuitem
+           JOIN sys_group_menuitem ON sys_menuitem.id = sys_menuitem_id
+           JOIN sys_group ON sys_group.id = sys_group_menuitem.sys_group_id
+           JOIN sys_group_user ON sys_group.id = sys_group_user.sys_group_id
+           WHERE sys_user_id = %d
+             AND sys_menuitem.module = %s AND sys_menuitem.page = %s
+EOS;
+       $sql = sprintf($sql, $_SESSION['sys']['uid'], $this->db->quote($hallinta->module()), $this->db->quote($hallinta->page()));
+
+       return $this->db->fetchValue($sql) > 0;
+    }
+
+    public function mayEdit()
+    {
+       $hallinta = Hallinta::instance();
+
+       $sql = <<<EOS
+           SELECT count(*)
+           FROM sys_menuitem
+           JOIN sys_group_menuitem ON sys_menuitem.id = sys_menuitem_id
+           JOIN sys_group ON sys_group.id = sys_group_menuitem.sys_group_id
+           JOIN sys_group_user ON sys_group.id = sys_group_user.sys_group_id
+           WHERE sys_menuitem.edit = 1 AND sys_user_id = %d
+             AND sys_menuitem.module = %s AND sys_menuitem.page = %s
+EOS;
+       $sql = sprintf($sql, $_SESSION['sys']['uid'], $this->db->quote($hallinta->module()), $this->db->quote($hallinta->page()));
+
+       return $this->db->fetchValue($sql) > 0;
+    }
+
+    public function isRegular()
+    {
+       $hallinta = Hallinta::instance();
+
+       $sql = <<<EOS
+           SELECT count(*)
+           FROM sys_menuitem
+           JOIN sys_group_menuitem ON sys_menuitem.id = sys_menuitem_id
+           JOIN sys_group ON sys_group.id = sys_group_menuitem.sys_group_id
+           JOIN sys_group_user ON sys_group.id = sys_group_user.sys_group_id
+           WHERE sys_menuitem.shadow = 0 AND sys_user_id = %d
+             AND sys_menuitem.module = %s AND sys_menuitem.page = %s
+EOS;
+       $sql = sprintf($sql, $_SESSION['sys']['uid'], $this->db->quote($hallinta->module()), $this->db->quote($hallinta->page()));
+
+       return $this->db->fetchValue($sql) > 0;
+    }
+
+    public function ajaxGetParent(Array $data)
+    {
+       return ['list' => $this->getMenus()];
+    }
+
+
+    public function ajaxGetModules(Array $data)
+    {
+       return ['list' => $this->getModules()];
+    }
+
+    public function ajaxGetPages(Array $data)
+    {
+       return ['list' => $this->getPages($data['module'])];
+    }
+}
diff --git a/class/scannerbase.class.php b/class/scannerbase.class.php
new file mode 100644 (file)
index 0000000..d0f52e6
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+class ScannerBase implements ScannerInterface {
+    public const LEFT = 1;
+    public const FLIP = 2;
+    public const RIGHT = 3;
+    protected static $scanner = false;
+
+    public static function getScanner()
+    {
+       if (static::$scanner === false) {
+           $ScannerClass = defined('DOCUMENT_FILES_SCANNER_CLASS') ? DOCUMENT_FILES_SCANNER_CLASS : 'ScannerBase';
+           static::$scanner = new $ScannerClass();
+
+           if (!is_a(static::$scanner, 'ScannerInterface'))
+               throw new Exception("DOCUMENT_FILES_SCANNER_CLASS does not implement ScannerInterface");
+       }
+
+       return static::$scanner;
+    }
+
+    public function hasScanner()
+    {
+       return false;
+    }
+
+    public function getResolutionList()
+    {
+    }
+
+    public function setResolution($id)
+    {
+    }
+
+    public function scanIntoPNM($outfile)
+    {
+       $scanimage = '/usr/bin/scanimage';
+
+       if (!file_exists($scanimage))
+           throw new Exception("Package sane-utils not installed");
+
+       if (!is_executable($scanimage))
+           throw new Exception("$scanimage not executable");
+
+       $cmd = sprintf("%s --format pnm > %s",
+                      $scanimage,
+                      escapeshellarg($outfile));
+
+       $this->execute($cmd);
+    }
+
+    public function convertPNMtoPNG($infile, $outfile)
+    {
+       $pnmtopng = '/usr/bin/pnmtopng';
+
+       if (!file_exists($pnmtopng))
+           throw new Exception("Package netpbm not installed");
+
+       if (!is_executable($pnmtopng))
+           throw new Exception("$pnmtopng not executable");
+
+       $cmd = sprintf("cat %s | %s > %s",
+                      escapeshellarg($infile),
+                      $pnmtopng,
+                      escapeshellarg($outfile));
+
+       $this->execute($cmd);
+    }
+
+    public function rotatePNM($file, $direction)
+    {
+       $convert = '/usr/bin/convert';
+
+       if (!file_exists($convert))
+           throw new Exception("Package imagemagick not installed");
+
+       if (!is_executable($convert))
+           throw new Exception("$scanimage not executable");
+
+       switch (intval($direction)) {
+       case static::RIGHT: $degrees = 90; break;
+       case static::FLIP: $degrees = 180; break;
+       case static::LEFT: $degrees = 270; break;
+       default: throw new Exception("Invalid rotation command");
+       }
+
+       $cmd = sprintf("convert -rotate %d %s %s && mv -f %s %s",
+                      $degrees,
+                      escapeshellarg($file), escapeshellarg($file.'.pnm'),
+                      escapeshellarg($file.'.pnm'), escapeshellarg($file));
+
+       $this->execute($cmd);
+    }
+
+    public function convertPNMtoPDF($infile, $outfile)
+    {
+       $pnmtops = '/usr/bin/pnmtops';
+       $pstopdf = '/usr/bin/ps2pdf';
+
+       if (!file_exists($pnmtops))
+           throw new Exception("Package netpbm not installed");
+
+       if (!is_executable($pnmtops))
+           throw new Exception("$pnmtops not executable");
+
+       if (!file_exists($pstopdf))
+           throw new Exception("Package ghostscript not installed");
+
+       if (!is_executable($pstopdf))
+           throw new Exception("$pnmtops not executable");
+
+       $cmd = sprintf("cat %s | %s | %s - > %s",
+                      escapeshellarg($infile),
+                      $pnmtops,
+                      $pstopdf,
+                      escapeshellarg($outfile));
+
+       $this->execute($cmd);
+    }
+
+    public function mergePDF(Array $infiles, $outfile)
+    {
+       $pdftk = '/usr/bin/pdftk';
+
+       if (!file_exists($pdftk))
+           throw new Exception("Package pdftk not installed");
+
+       if (!is_executable($pdftk))
+           throw new Exception("$pdftk not executable");
+
+       $cmd = $pdftk;
+       foreach ($infiles as $infile)
+           $cmd .= ' ' . escapeshellarg($infile);
+       $cmd .= ' cat output ' . escapeshellarg($outfile);
+
+       $this->execute($cmd);
+    }
+
+    /***********************************************************************/
+    protected function execute($cmd)
+    {
+       if (defined('DEBUG') && DEBUG == true)
+           debug($cmd);
+
+       system($cmd, $ret);
+
+       if ($ret != 0) {
+           $text = sprintf("Program execution resulted in exit code %d", $ret);
+           error_log($text);
+           error_log($cmd);
+           throw new Exception($text);
+       }
+
+       return true;
+    }
+
+    public function getPDF(Array $infiles)
+    {
+    }
+}
\ No newline at end of file
diff --git a/class/scannerinterface.class.php b/class/scannerinterface.class.php
new file mode 100644 (file)
index 0000000..b095a88
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+interface ScannerInterface {
+
+    public function hasScanner();
+
+    public function getResolutionList();
+
+    public function setResolution($id);
+
+    public function scanIntoPNM($outfile);
+
+    public function rotatePNM($file, $direction);
+
+    public function convertPNMtoPNG($infile, $outfile);
+
+    public function convertPNMtoPDF($infile, $outfile);
+
+    public function mergePDF(Array $infiles, $outfile);
+}
\ No newline at end of file
diff --git a/class/select.class.php b/class/select.class.php
new file mode 100644 (file)
index 0000000..08a96b5
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+class Select {
+  protected $id = false;
+  protected $title = false;
+  protected $default = false;
+  protected $options = array();
+  protected $function = false;
+  protected $selected = false;
+
+  public function __construct(array $info)
+  {
+    if (!array_key_exists('function', $info))
+      throw new Exception('Property function missing');
+    else $this->function = $info['function'];
+
+    if (array_key_exists('id', $info)) $this->id = $info['id'];
+    if (array_key_exists('title', $info)) $this->title = $info['title'];
+    if (array_key_exists('default', $info)) $this->default = $info['default'];
+    if (array_key_exists('selected', $info)) $this->selected = $info['selected'];
+
+    if (array_key_exists('options', $info)) {
+      if (is_array($info['options'])) {
+       foreach ($info['options'] as $option) {
+         if (is_array($option))
+           $this->options[] = array($option[0], $option[1]);
+         else
+           $this->options[] = array($option, $option);
+       }
+      } elseif (is_string($info['options'])) {
+       $this->loadOptions($info['options']);
+      } else
+         throw new Exception('Unknown options type');
+    }
+  }
+
+  protected function loadOptions($sql)
+  {
+    global $db;
+    foreach ($db->fetchObjectList($sql) as $row)
+      $this->options[] = array($row->id, $row->text);
+  }
+
+  public function toString()
+  {
+    $attr = array();
+    if ($this->id) $attr[] = sprintf('id="%s"', $this->id);
+    if ($this->title) $attr[] = sprintf('title="%s"', $this->title);
+    if ($this->function) $attr[] = sprintf('onchange="%s(event, this)"', $this->function);
+
+    $code = array();
+    $code[] = sprintf('<select %s>', implode(' ', $attr));
+    if ($this->default) $code[] = sprintf('<option value="">%s</option>', $this->default);
+    foreach ($this->options as $option)
+      $code[] = sprintf('<option value="%s"%s>%s</option>',
+                       $option[0],
+                       $this->selected !== false && $this->selected == $option[0] ? ' selected' : '',
+                       $option[1]);
+    $code[] = '</select>';
+
+    return implode('', $code);
+  }
+}
diff --git a/class/singleton.class.php b/class/singleton.class.php
new file mode 100644 (file)
index 0000000..cb9c07c
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+abstract class Singleton {
+  protected static $instance = false;
+
+  final public static function instance()
+  {
+    if (static::$instance === false) {
+      $className = get_called_class();
+      static::$instance = new $className();
+    }
+
+    return static::$instance;
+  }
+
+  final private function __construct() {
+    if (method_exists($this, '___construct'))
+      $this->___construct();
+  }
+}
diff --git a/class/styles.class.php b/class/styles.class.php
new file mode 100644 (file)
index 0000000..8242bfb
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+class Styles extends Singleton {
+  protected static $instance = false;
+  protected $code = array();
+  protected $files = array();
+
+  public function add($code)
+  {
+    $this->code[] = $code;
+  }
+
+  public function file($path)
+  {
+    $hallinta = Hallinta::instance();
+
+    if (strpos($path, '/') === false) {
+       if (!file_exists($hallinta->basedir() . $path))
+           $path = sprintf('%s/%s/%s', HALLINTA_MODULEDIR, $hallinta->module(), $path);
+    }
+
+    $info = pathinfo($path);
+
+    $minfile = sprintf('%s%s/%s.min.%s', $hallinta->basedir(), $info['dirname'], $info['filename'], $info['extension']);
+    if (file_exists($minfile)) {
+      $origpath = $hallinta->basedir().$path;
+      if (filemtime($minfile) > filemtime($origpath))
+       $path = sprintf('%s/%s.min.%s', $info['dirname'], $info['filename'], $info['extension']);
+    }
+
+    $this->files[$path] = true;
+  }
+
+  public function toString()
+  {
+    $ret = '';
+    foreach (array_keys($this->files) as $file)
+      $ret .= sprintf('<link href="%s%s" rel="stylesheet" type="text/css">'."\n", Hallinta::instance()->urlbase(), $file);
+
+    if (count($this->code)) {
+      $ret .= '<style type="text/css">'."\n";
+      $ret .= implode("\n", $this->code);
+      $ret .= "</style>\n";
+    }
+
+    return $ret;
+  }
+
+}
diff --git a/class/template.class.php b/class/template.class.php
new file mode 100644 (file)
index 0000000..ed99b11
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+class Template {
+    private $path;
+    private $data = array();
+
+    public static function render($name, Array $data)
+    {
+       $template = new Template($name);
+       $template->addData($data);
+       return $template->fillIn();
+    }
+
+    public function __construct($name)
+    {
+        if (strpos($name, '../') !== false)
+            throw new Exception("Template name contains illegal path component.");
+
+        $this->path = $this->getPath($name);
+
+        if ($this->path === false)
+            throw new Exception("Template $name not found in templates/ directory.");
+
+        if (!is_readable($this->path))
+            throw new Exception("Template $name not readable.");
+    }
+
+    public function id()
+    {
+       return $this->path;
+    }
+
+    protected function getPath($name)
+    {
+       $hallinta = Hallinta::instance();
+
+       $path = $hallinta->basedir() . 'masks/' . $hallinta->module() . '/templates/' . $name . '.phtml';
+       if (file_exists($path)) return $path;
+
+       $path = $hallinta->basedir() . 'templates/' . $name . '.phtml';
+       if (file_exists($path)) return $path;
+
+       return false;
+    }
+
+    public function add($name, $value)
+    {
+        $this->data[$name] = $value;
+    }
+
+    public function addData(Array $data)
+    {
+       foreach ($data as $name => $value)
+           $this->data[$name] = $value;
+    }
+
+    public function fillIn()
+    {
+        extract($this->data);
+
+       try {
+           ob_start();
+           include($this->path);
+           $text = ob_get_clean();
+       } catch (Exception $e) {
+           $text = ob_get_clean();
+           throw new Exception($e->getMessage());
+       } catch (Error $e) {
+           $text = ob_get_clean();
+           throw new Exception($e->getMessage());
+       }
+
+        return $text;
+    }
+
+    public function ajaxLoad(Array $data)
+    {
+       $this->addData($data);
+       return $this->fillIn();
+    }
+}
index de60dc4..2a1c982 100644 (file)
@@ -8,6 +8,10 @@ define('SESSION_NAME', 'HALLINTA');
 # define('SESSION_PATH', '/hallinta/');
 # Cookie lifetime, defaults to 24 hours
 # define('SESSION_LIFETIME', 60*60*2);
+# define('FAVICON', 'images/favicon.ico');
+
+# Use rewriting via .htaccess
+define('REWRITE_URLS', true);
 
 # defaults to images/login.jpg
 # define('LOGIN_IMG', 'images/login_neu.jpg');
@@ -27,8 +31,11 @@ define('MAIL_FROM', 'www-data@virka.home.infodrom.org');
 define('MAIL_FROM_NAME', 'Infodrom Virka');
 define('MAIL_ERROR', 'joey@finlandia.infodrom.org');
 
+# Copy a deleted row into <table>_deleted
 define('DELETE_COPY', true);
 
+# Allow access to document scanner
+# define('DOCUMENT_FILES_SCANNER_CLASS', 'ScannerBase');
+
 define('DEBUG', false);
 
-?>
diff --git a/dropdown.css b/dropdown.css
deleted file mode 100644 (file)
index 2d74cd5..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-@charset "UTF-8";
-
-/**
- * Horizontal CSS Drop-Down Menu Module
- *
- * @file               dropdown.css
- * @package            Dropdown
- * @version            0.7.1
- * @type               Transitional
- * @stacks             597-599
- * @browsers   Windows: IE6+, Opera7+, Firefox1+
- *                             Mac OS: Safari2+, Firefox2+
- *
- * @link               http://www.lwis.net/
- * @copyright  2006-2008 Live Web Institute. All Rights Reserved.
- *
- * http://lwis.net/free-css-drop-down-menu/dropdown.simple.horizontal.html
- *
- */
-
-div.menu {
- padding-left: 20px;
- height: 27px;
-}
-
-ul.dropdown,
-ul.dropdown li,
-ul.dropdown ul {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-ul.dropdown {
- position: relative;
- z-index: 597;
- float: left;
-}
-
-ul.dropdown li {
- float: left;
- line-height: 1.0em;
- vertical-align: middle;
- zoom: 1;
-}
-
-ul.dropdown li.hover,
-ul.dropdown li:hover {
- position: relative;
- z-index: 599;
- cursor: default;
-}
-
-ul.dropdown ul {
- visibility: hidden;
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 598;
- width: 100%;
-}
-
-ul.dropdown ul li {
- float: none;
-}
-
-ul.dropdown ul ul {
- top: 1px;
- left: 99%;
-}
-
-ul.dropdown li:hover > ul {
- visibility: visible;
-}
-
-/* top menu items */
-ul.dropdown {
- font-weight: normal;
-}
-
-ul.dropdown li {
- padding: 7px 10px;
- border-style: solid;
- border-width: 1px 1px 1px 0;
- border-color: #fff #d9d9d9 #d9d9d9;
- background-color: #f6f6f6;
- color: #000;
-}
-
-ul.dropdown li.hover,
-ul.dropdown li:hover {
- background-color: #eee;
- color: #000;
-}
-
-ul.dropdown a:link,
-ul.dropdown a:visited   { color: #000; text-decoration: none; }
-ul.dropdown a:hover             { color: #000; }
-ul.dropdown a:active    { color: #ffa500; }
-
-
-/* -- level mark -- */
-
-ul.dropdown ul {
- width: 150px;
- margin-top: 1px;
-}
-
-ul.dropdown ul li {
- font-weight: normal;
-}
-
diff --git a/images/bg_header.png b/images/bg_header.png
new file mode 100644 (file)
index 0000000..61dd5ec
Binary files /dev/null and b/images/bg_header.png differ
diff --git a/images/icons/csv.gif b/images/icons/csv.gif
new file mode 100644 (file)
index 0000000..82eecd8
Binary files /dev/null and b/images/icons/csv.gif differ
diff --git a/images/icons/download.gif b/images/icons/download.gif
deleted file mode 100644 (file)
index b7543d6..0000000
Binary files a/images/icons/download.gif and /dev/null differ
diff --git a/images/icons/download.png b/images/icons/download.png
new file mode 100644 (file)
index 0000000..b6ec764
Binary files /dev/null and b/images/icons/download.png differ
diff --git a/images/icons/ebook.gif b/images/icons/ebook.gif
new file mode 100644 (file)
index 0000000..e74a8d9
Binary files /dev/null and b/images/icons/ebook.gif differ
diff --git a/images/icons/edit.png b/images/icons/edit.png
new file mode 100644 (file)
index 0000000..8091887
Binary files /dev/null and b/images/icons/edit.png differ
diff --git a/images/icons/gnumeric.png b/images/icons/gnumeric.png
new file mode 100644 (file)
index 0000000..cda04a9
Binary files /dev/null and b/images/icons/gnumeric.png differ
diff --git a/images/icons/html.png b/images/icons/html.png
new file mode 100644 (file)
index 0000000..9a40e74
Binary files /dev/null and b/images/icons/html.png differ
diff --git a/images/icons/link.png b/images/icons/link.png
new file mode 100644 (file)
index 0000000..4b710b0
Binary files /dev/null and b/images/icons/link.png differ
diff --git a/images/icons/liste.gif b/images/icons/liste.gif
new file mode 100644 (file)
index 0000000..84b03ef
Binary files /dev/null and b/images/icons/liste.gif differ
diff --git a/images/icons/menu.png b/images/icons/menu.png
new file mode 100644 (file)
index 0000000..630de5b
Binary files /dev/null and b/images/icons/menu.png differ
index d6caea7..6d722b0 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1,35 +1,49 @@
 <?php
 require_once('init.php');
-require_once('lib/menu.php');
 
-$jsfiles = array('lib/json_parse.js');
-$jscode = '';
-$menu = menu();
+$styles = Styles::instance();
+$javascript = JavaScript::instance();
+
+$javascript->file('lib/jquery-2.1.0.min.js');
+$javascript->file('lib/jquery.event.ue.js');
+$javascript->file('lib/jquery.udraggable.js');
+
+$javascript->file('lib/json_parse.js');
+$styles->file('stylesheet.css');
+$styles->file('style.css');
+if (!empty($_SESSION['sys']['login']))
+  $styles->file('theme.php');
 $html = process();
 $debug = debug_info();
 
 # max 8h
-$gcml = ini_get('session.gc_maxlifetime');
-$refresh = $gcml < 8*60*60 ? $gcml - 60 : 8*60*60 - 60;
-
+if (!empty($_SESSION['sys']['login'])) {
+  $gcml = ini_get('session.gc_maxlifetime');
+  $refresh = $gcml < 8*60*60 ? $gcml : 8*60*60;
+  $javascript->add(sprintf('Hallinta.autoLogoutTimeout = %d;', $refresh));
+  $javascript->add(sprintf('Hallinta.token = \'%s\';', $_SESSION['token']));
+  $javascript->add('Hallinta.baseURL = "'.Hallinta::instance()->urlbase().'";');
+  $javascript->add("Hallinta.menuBase = '".Hallinta::instance()->parentMenu()."';");
+}
 ?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="de">
 <head>
 <title><?=PAGETITLE?></title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-<?=load_js($jsfiles, $jscode);?>
-<link href="stylesheet.css" rel="stylesheet" type="text/css">
-<link href="style.css" rel="stylesheet" type="text/css">
-<link href="dropdown.css" rel="stylesheet" type="text/css">
-<link href="theme.php" rel="stylesheet" type="text/css">
-<?=load_style($style);?>
+<?php if (defined('FAVICON')) printf('<link rel="shortcut icon" href="%s">', FAVICON); ?>
+<?=$styles->toString()?>
 </head>
 <body>
+<?php if (isset($refresh)): ?>
 <div id="logout_refresh" style="display: none;"><?=$refresh?></div>
+<?php endif ?>
 <div id="header">
-<h1><?=TITLE?></h1>
-<?=$menu; ?>
+<div id="actionbox"><?=Actions::instance()->toString()?></div>
+<div id="title"><?=TITLE?></div>
+<div id="titlesep"><?=strlen(Hallinta::instance()->getTitle())?'&mdash;':'';?></div>
+<div id="pagetitle"><?=Hallinta::instance()->getTitle()?></div>
 </div>
 
 <div class="content">
@@ -37,5 +51,6 @@ $refresh = $gcml < 8*60*60 ? $gcml - 60 : 8*60*60 - 60;
 </div>
 
 <?=$debug; ?>
+<?=$javascript->toString()?>
 </body>
 </html>
index fb36426..ce1a796 100644 (file)
--- a/init.php
+++ b/init.php
@@ -1,6 +1,7 @@
 <?php
 
 require_once('config.php');
+require_once('class/autoload.class.php');
 
 if (!defined('SESSION_PATH')) define('SESSION_PATH', '/');
 if (!defined('SESSION_LIFETIME')) define('SESSION_LIFETIME', 60*60*56);
@@ -9,9 +10,10 @@ session_name(SESSION_NAME);
 session_set_cookie_params(SESSION_LIFETIME, SESSION_PATH);
 session_start();
 
+define('HALLINTA_MODULEDIR', 'masks');
+
 require_once('lib/general.php');
 
+check_empty_session();
 db_connect();
 check_session();
-
-?>
index 4b46e45..5843eeb 100644 (file)
+var Hallinta = {
+    /**
+     * Options
+     *
+     * closeDialogAfterInsert [bool] close details dialog after inserting a new item
+     * clearDialogAfterInsert [bool] clear details dialog after inserting a new item
+     * closeDialogAfterSave   [bool] close details dialog after saving a item
+     * clearDialogAfterSave   [bool] clear details dialog after saving a item
+     * closeDialogAfterDelete [bool] close details dialog after deleting a item
+     * fetchItemAfterInsert   [bool] fetch recently inserted item after inserting
+     *
+     * pageInit               [function()]     Function to be called after page has been loaded
+     * postLoadForm           [function(data)] Function to be called after edit form has been loaded
+     * postFetch              [function(data)] Function to be called after fetching an item
+     * preSave                [function()]     Function to be called before saving an item
+     * postSave               [function(data)] Function to be called after saving an item
+     * preInsert              [function()]     Function to be called before inserting a new item
+     * postInsert             [function(data)] Function to be called after inserting a new item
+     * preDelete              [function()]     Function to be called before deleting an item
+     * postDelete             [function(data)] Function to be called after deleting an item
+     * drillDown              [function(e,id)] Function to be called after clicking on a cell
+     */
+    closeDialogAfterInsert: true,
+    clearDialogAfterInsert: true,
+    closeDialogAfterSave: true,
+    clearDialogAfterSave: true,
+    closeDialogAfterDelete: true,
+    fetchItemAfterInsert: false,
+    pageInit: false,
+    postLoadForm: false,
+    postFetch: false,
+    preSave: false,
+    postSave: false,
+    preInsert: false,
+    postInsert: false,
+    postDelete: false,
+    drillDown: false,
+    /* End options */
+    grid: false,
+    mainId: false,
+    pageSource: false,
+    baseURL: '',
+    menuBase: '',
+    autoLogoutTimeout: 15*60,
+    autoLogoutDialog: false,
+    isMobile: false,
+    canEdit: false,
+    editDialog: false,
+    editFormCheck: {},
+    gridMenuColumns: {},
+    seconds: {},
+    messageDiv: false,
+    menuDialog: false
+};
 /*
  * Small AJAX framework
  */
+function ajax_request_response(data, oncomplete)
+{
+    if (typeof data.error == 'string') {
+       if (typeof data.errormsg == 'string')
+           error(data.errormsg);
+       else
+           error(data.error);
+       Hallinta.hideMsg();
+       alert(data.error);
+
+       if (typeof data.logout != 'undefined' && data.logout == true)
+           window.location.href = 'index.php?logout=true&auto=true';
+    } else {
+       if (typeof oncomplete == 'function')
+           oncomplete(data);
+    }
+
+    reset_logout();
+}
+
 function ajax_request_callback(req)
 {
     if (req.readyState == 4 && req.status == 200) {
        var data = json_parse(req.responseText);
 
-       if (data && typeof data.error == 'string') {
-           if (typeof data.errormsg == 'string')
-               error(data.errormsg);
-           else
-               error(data.error);
-           alert(data.error);
-           if (typeof data.logout != 'undefined' && data.logout == true)
-               window.location.href = 'index.php?logout=true';
-       } else if (req.oncomplete)
-           req.oncomplete(data);
+       if (data) {
+           ajax_request_response(data, req.oncomplete)
+       } else {
+           alert("Unparsable response");
+       }
     }
 }
 
 function ajax_request(func,params,oncomplete)
 {
+    if (func.indexOf('/') > -1) {
+       if (params === null || params == '') {
+           params = {route: func};
+           if (Hallinta.token) params['token'] = Hallinta.token;
+           if (Hallinta.pageSource) params['source'] = Hallinta.pageSource;
+       } else if (typeof params == 'object') {
+           params['route'] = func;
+           if (Hallinta.token) params['token'] = Hallinta.token;
+           if (Hallinta.pageSource) params['source'] = Hallinta.pageSource;
+       } else {
+           params = 'route='+func+'&'+params
+           if (Hallinta.token) params += '&token=' + Hallinta.token;
+           if (Hallinta.pageSource) params += '&source=' + Hallinta.pageSource;
+       }
+
+        $.ajax({
+           method: 'POST',
+           url: Hallinta.baseURL + 'ajax/ajax.php',
+           data: params,
+           dataType: 'json'
+       }).done(function(data){
+           ajax_request_response(data, oncomplete);
+       });
+
+       return;
+    }
+
     var req = new XMLHttpRequest();
     if (!req) return;
 
-    var params = 'func=' + func + '&' + params;
-    req.open ("POST", 'ajax/ajax.php');
+    var reqParams = 'func=' + func;
+    if (Hallinta.token) reqParams += '&token=' + Hallinta.token;
+    if (Hallinta.pageSource) reqParams += '&source=' + Hallinta.pageSource;
+    if (typeof params == 'string') {
+       if (params && params.length) reqParams +='&' + params;
+    } else if (typeof params == 'object') {
+       for (name in params)
+           reqParams += '&' + name + '=' + escape(params[name]);
+    }
+
+    req.open ("POST", Hallinta.baseURL + 'ajax/ajax.php');
     req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
-    req.setRequestHeader("Content-length", params.length);
-    req.setRequestHeader("Connection", "close");
     req.onreadystatechange = function() { ajax_request_callback(req); }
     if (typeof oncomplete == 'function')
        req.oncomplete = oncomplete;
-    req.send(params);
+    req.send(reqParams);
     info('');
-    reset_logout();
 }
 
-var logout_timer = false;
-window.setTimeout(reset_logout, 4000);
+function ajax_callback(name,params,oncomplete)
+{
+    if (typeof params == 'string' && params.length)
+       params = 'callback=' + name + '&' + params;
+    else if (params !== null && typeof params == 'object')
+       params['callback'] = name;
+    else
+       params = 'callback=' + name;
+
+    ajax_request('function', params, oncomplete);
+}
+
+Hallinta.popupMobilePosition = function(popup) {
+    if (!Hallinta.isMobile) return;
+
+    $(popup.contentDiv).parents('div.ricoShadow').css('top', '25px')
+};
+
+Hallinta.openEditDialog = function() {
+    Hallinta.editDialog.openPopup();
+    focus_first_element('#form_edit');
+};
+
+Hallinta.enhanceGridOptions = function(options) {
+    if (typeof options.onscroll == 'string') {
+       if (options.onscroll == 'gridOnScroll')
+           options.onscroll = gridOnScroll;
+       else error('Unknown options.onscroll');
+    }
+    if (typeof options.click == 'string') {
+       options.click = eval(options.click);
+    }
+
+    for (var i=0; i < options.columnSpecs.length; i++) {
+       if (typeof options.columnSpecs[i].control == 'string')
+           options.columnSpecs[i].control = eval(options.columnSpecs[i].control);
+    }
+};
+
+Hallinta.fixGridColumns = function(gridname) {
+    $('div#grid_' + gridname + '_outerDiv tr.ricoLG_hdg div.ricoLG_col').each(function(i,e){
+       if (typeof $(e).attr('style') !== 'undefined') {
+           var width = $(e).css('width');
+           width = parseInt(width.substr(0,width.length-2),10);
+           $(e).css('width', (width+1).toString());
+       }
+    });
+
+    $('div#grid_' + gridname + '_scrollContainerDiv .ricoLG_scrollTabDiv div.ricoLG_col').each(function(i,e){
+       if (typeof $(e).attr('style') !== 'undefined') {
+           var width = $(e).css('width');
+           width = parseInt(width.substr(0,width.length-2),10);
+           $(e).css('width', (width+1).toString());
+       }
+    });
+};
+
+Hallinta.loadMainGridCallback = function(json) {
+    Hallinta.showMsg('Die Liste wird aufgebaut...');
+    $('div#div_grid').html(json.html);
+    Hallinta.enhanceGridOptions(json.options);
+    Hallinta.grid = new Rico.LiveGrid('grid_'+json.gridname,
+                                     new Rico.Buffer.AjaxSQL(Hallinta.baseURL + 'ajax/ricoXMLquery.php'),
+                                     json.options);
+    Hallinta.grid.menu = new Rico.GridMenu();
+    Hallinta.grid.edit = new Rico.TableEdit(Hallinta.grid);
+    Hallinta.grid.edit.responseHandler = function(xhr) { Hallinta.gridEditHandler(Hallinta.grid, xhr); };
+    Hallinta.grid.menu.options.dataMenuHandlerOriginal = Hallinta.grid.menu.options.dataMenuHandler;
+    Hallinta.grid.menu.options.dataMenuHandler = Hallinta.dataMenuHandler;
+    Hallinta.fixGridColumns(json.gridname);
+    Hallinta.hideMsg();
+};
+
+Hallinta.loadMainGrid = function() {
+    Hallinta.showMsg('Die Liste wird geladen...');
+    ajax_request('grid', 'name=main', Hallinta.loadMainGridCallback);
+};
+
+Hallinta.openSecondaryGridCallback = function(json) {
+    Hallinta.enhanceGridOptions(json.options);
+    Hallinta.seconds[json.name] = {};
+    var window_options = {};
+    if (typeof json.width !== 'undefined')
+       window_options['width'] = json.width + 'px';
+    Hallinta.seconds[json.name].popup = new Rico.Window(json.title, window_options);
+    Rico.addClass(Hallinta.seconds[json.name].popup.container, 'gridPopup');
+
+    $(Hallinta.seconds[json.name].popup.contentDiv).html(json.html);
+    Hallinta.seconds[json.name].popup.openPopup();
+
+    var p = 'second_id=' + (Hallinta.mainId ? Hallinta.mainId : 0);
+    Hallinta.seconds[json.name].grid = new Rico.LiveGrid('grid_'+json.gridname,
+                                                        new Rico.Buffer.AjaxSQL(Hallinta.baseURL + 'ajax/ricoXMLquery.php',
+                                                                                {requestParameters: [p]}),
+                                                        json.options);
+    Hallinta.seconds[json.name].grid.menu = new Rico.GridMenu();
+    Hallinta.seconds[json.name].edit = new Rico.TableEdit(Hallinta.seconds[json.name].grid);
+    Hallinta.seconds[json.name].edit.responseHandler = function(xhr) { Hallinta.gridEditHandler(Hallinta.seconds[json.name].grid, xhr); };
+    Hallinta.seconds[json.name].grid.menu.options.dataMenuHandlerOriginal = Hallinta.seconds[json.name].grid.menu.options.dataMenuHandler;
+    Hallinta.seconds[json.name].grid.menu.options.dataMenuHandler = Hallinta.dataMenuHandler;
+    Hallinta.fixGridColumns(json.gridname);
+
+    Hallinta.seconds[json.name].popup.centerPopup();
+};
+
+Hallinta.openSecondaryGrid = function(event, name) {
+    if (typeof Hallinta.seconds[name] == 'undefined') {
+       ajax_request('grid', 'name='+name, Hallinta.openSecondaryGridCallback);
+    } else {
+       var p = 'second_id=' + (Hallinta.mainId ? Hallinta.mainId : 0);
+       Hallinta.seconds[name].grid.buffer.options.requestParameters = [p];
+       grid_update(Hallinta.seconds[name].grid);
+
+       Hallinta.seconds[name].popup.openPopup();
+    }
+};
+
+Hallinta.registerGridMenu = function(name, col, options) {
+    if (typeof Hallinta.gridMenuColumns[name] != 'object')
+       Hallinta.gridMenuColumns[name] = {};
+       Hallinta.gridMenuColumns[name][col] = options;
+};
+
+Hallinta.gridEditHandler = function(grid, xhr)
+{
+    var data = $.parseJSON(xhr.responseText);
+
+    $('#'+grid.tableId+'_editResponseDiv').hide();
+    if (typeof data.error == 'string')
+       Hallinta.showMsg('ERROR: ' + data.error, {timeout: 10});
+    else
+       Hallinta.showMsg('Item successfully saved', {timeout: 2});
+
+    grid_update(grid);
+}
+
+/**
+ * Hallinta.showMsg(text, opts)
+ *
+ * Options:
+ *   timeout  Seconds until auto-closing message window
+*/
+Hallinta.showMsg = function(text, opts) {
+    var options = {
+       timeout: false
+    };
+    if (typeof options == 'undefined') options = {};
+    Rico.extend(options, opts);
+
+    if (!Hallinta.messageDiv) {
+       Hallinta.messageDiv = $('<div>');
+       Hallinta.messageDiv.attr('id', 'message');
+       Hallinta.messageDiv.hide();
+       $(document.body).append(Hallinta.messageDiv);
+    }
+
+    Hallinta.messageDiv.text(text);
+    Hallinta.messageDiv.show();
+
+    Hallinta.messageDiv.css('position','absolute')
+       .css('top', ((window.innerHeight / 2) - (Hallinta.messageDiv.height() / 2)) + 'px')
+       .css('left', ((window.innerWidth / 2) - (Hallinta.messageDiv.width() / 2)) + 'px');
+
+    if (options.timeout)
+       setTimeout(Hallinta.hideMsg, options.timeout * 1000);
+
+    return false;
+};
+
+Hallinta.hideMsg = function() {
+    if (!Hallinta.messageDiv) return;
+    Hallinta.messageDiv.hide();
+};
+
+function page_init()
+{
+    if ($('div#div_grid').length) Hallinta.loadMainGrid();
+    if (Hallinta.canEdit) setTimeout(load_edit_form, 500);
+    if (typeof Hallinta.pageInit == 'function')
+       Hallinta.pageInit();
+}
+
+function seconds2time(seconds)
+{
+    var mins = Math.floor(seconds/60);
+    var secs = (seconds - mins*60).toString();
+    secs = (secs.length == 1 ? '0' : '') + secs;
+    return mins + ':' + secs;
+}
+
+function do_logout()
+{
+    window.location.href = 'index.php?logout=true&auto=true';
+    return false;
+}
+
+function prevent_logout()
+{
+    Hallinta.skipLogout = true;
+    ajax_request('menu', 'base=');
+    Hallinta.autoLogoutDialog.closePopup();
+}
+
+function prepare_logout()
+{
+    var centerDialog = false;
+    if (!Hallinta.autoLogoutDialog) {
+       Hallinta.autoLogoutDialog = new Rico.Window('Automatische Abmeldung', {zIndex: 100});
+       Rico.addClass(Hallinta.autoLogoutDialog.container, 'logoutDialog');
+       centerDialog = true;;
+    }
+
+    Hallinta.autoLogoutTimer = 60;
+
+    $(Hallinta.autoLogoutDialog.contentDiv).html([
+       '<div style="padding:10px">',
+       '<p>Keine Aktivität seit '+seconds2time(Hallinta.autoLogoutTimeout)+' Minuten.</p>',
+       '<p>Sie werden in <span id="autoLogoutTimer">'+seconds2time(Hallinta.autoLogoutTimer)+'</span> automatisch abgemeldet.</p>',
+       '<div style="text-align:center;">',
+       '<button style="padding:3px" onclick="return do_logout()">Abmelden</button>',
+       '&nbsp;&nbsp;&nbsp;',
+       '<button style="padding:3px" onclick="return prevent_logout()">Fortfahren</button>',
+       '</div>',
+       '</div>'
+    ].join(''));
+
+    if (centerDialog)
+        Hallinta.autoLogoutDialog.centerPopup();
+    else
+        Hallinta.autoLogoutDialog.openPopup();
+
+    Hallinta.skipLogout = false;
+    prepare_logout_timer();
+}
+
+function prepare_logout_timer()
+{
+    if (!Hallinta.autoLogoutTimer) {
+       do_logout();
+       return;
+    }
+
+    if (Hallinta.skipLogout)
+       return;
+
+    $('#autoLogoutTimer').text(seconds2time(Hallinta.autoLogoutTimer));
+    Hallinta.autoLogoutTimer--;
+    window.setTimeout(prepare_logout_timer, 1000);
+}
+
 function reset_logout()
 {
-    if (logout_timer)
-       window.clearTimeout(logout_timer);
+    if (Hallinta.autoLogoutTimeoutTimer)
+       window.clearTimeout(Hallinta.autoLogoutTimeoutTimer);
 
-    logout_timer = window.setTimeout(function() {
-           window.location.href = 'index.php?logout=true';
-       }, parseInt(document.getElementById('logout_refresh').innerHTML)*1000);
+    Hallinta.autoLogoutTimeoutTimer = window.setTimeout(function() {
+       prepare_logout();
+    }, (Hallinta.autoLogoutTimeout-60)*1000);
 }
+window.setTimeout(reset_logout,1000);
 
 var edit_hidden = false;
-var form_file_name = null;
-var form_file_content = null;
+var form_file_content = {};
 function form_file_change_onloadstart(e)
 {
     var button = document.getElementById('button_insert');
@@ -66,31 +416,37 @@ function form_file_change_onloadend(e)
     info('');
 }
 
-function form_file_change_onload(e)
+function form_file_change_onload(id,e)
 {
-    form_file_content = new Uint8ClampedArray(e.target.result);
+    form_file_content[id] = new Uint8ClampedArray(e.target.result);
 }
 
 function form_file_change(e)
 {
     var file = e.target.files[0];
     var reader = new FileReader();
-    reader.onload = form_file_change_onload;
+    reader.onload = form_file_change_onload.bind(this,this.id);
     reader.onloadend = form_file_change_onloadend;
     reader.onloadstart = form_file_change_onloadstart;
     reader.readAsArrayBuffer(file);
-    form_file_filename = file.name;
 }
 
-function ajax_form_submit(func,form,oncomplete)
+function build_multipart_data(func,formname,boundary)
 {
-    var boundary = "---------------------------" + (new Date).getTime();
     var CRLF  = "\r\n";
-    var req = new XMLHttpRequest();
-    if (!req) return;
 
     var parts = new Array();
 
+    var part = 'Content-Disposition: form-data; ';
+    part += 'name="source"' + CRLF + CRLF;
+    part += Hallinta.pageSource + CRLF;
+    parts.push(part);
+
+    var part = 'Content-Disposition: form-data; ';
+    part += 'name="token"' + CRLF + CRLF;
+    part += Hallinta.token + CRLF;
+    parts.push(part);
+
     if (func && func.length) {
         var part = 'Content-Disposition: form-data; ';
         part += 'name="func"' + CRLF + CRLF;
@@ -98,13 +454,34 @@ function ajax_form_submit(func,form,oncomplete)
         parts.push(part);
     }
 
-    for (var i=0; i < form.childNodes.length; i++) {
+    var req = new XMLHttpRequest();
+    if (!req) return;
+    if (typeof req.sendAsBinary == 'undefined' && typeof req.send == 'function')
+       var func_use_send = '1';
+    else
+       var func_use_send = '0';
+
+    var part = 'Content-Disposition: form-data; ';
+    part += 'name="func_use_send"' + CRLF + CRLF;
+    part += func_use_send + CRLF;
+    parts.push(part);
+    part = '';
+
+    if (!$(formname).length) {
+       error('Formular nicht gefunden');
+       alert('Formular nicht gefunden');
+       return;
+    }
+
+    var childNodes = $(formname).get(0).childNodes;
+    for (var i=0; i < childNodes.length; i++) {
        var part = '';
-       var element = form.childNodes[i];
+       var element = childNodes[i];
        var fieldName = element.name;
 
        if (element.nodeName.toLowerCase() == 'input' &&
-           element.type.toLowerCase() == 'file') {
+           element.type.toLowerCase() == 'file' &&
+           element.files.length) {
 
            if (typeof element.files[0].getAsBinary != 'function' &&
                typeof window.FileReader == 'undefined') {
@@ -118,17 +495,17 @@ function ajax_form_submit(func,form,oncomplete)
                var binary = element.files[0].getAsBinary();
                var filename = element.files[0].fileName;
            } catch(err) {
-               if (form_file_content) {
+               if (form_file_content[element.id]) {
                    var binary = '';
 
-                   for (var j=0; j < form_file_content.length; j++)
-                       binary += String.fromCharCode(form_file_content[j]);    
+                   for (var j=0; j < form_file_content[element.id].length; j++)
+                       binary += String.fromCharCode(form_file_content[element.id][j]);
 
-                   var filename = form_file_filename;
+                   var filename = element.files[0].name;
                } else {
                    error('Problem beim Lesen der Datei');
                    alert("Problem beim Lesen der Datei\n" +
-                         element.files[0].fileName + "\n" +
+                         element.files[0].name + "\n" +
                          "Eventuell stimmt das Encoding nicht\n" +
                          "Die Daten wurden nicht gespeichert");
                    return;
@@ -159,56 +536,67 @@ function ajax_form_submit(func,form,oncomplete)
         parts.push(part);
     }
 
+    if (typeof filename == 'string')
+       Hallinta.showMsg('Die Daten werden gespeichert.  Das kann etwas dauern...');
+
     var params = "--" + boundary + CRLF;
     params += parts.join("--" + boundary + CRLF);
     params += "--" + boundary + "--" + CRLF;
 
-    req.open ("POST", 'ajax/ajax.php');
+    return params;
+}
+
+function ajax_form_submit(func,formname,oncomplete)
+{
+    var boundary = "---------------------------" + (new Date).getTime();
+    var params = build_multipart_data(func,formname,boundary);
+
+    var req = new XMLHttpRequest();
+    if (!req) return;
+
+    req.open ("POST", Hallinta.baseURL + 'ajax/ajax.php');
     req.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
-    req.setRequestHeader("Connection", "close");
     req.onreadystatechange = function() { ajax_request_callback(req); }
     if (typeof oncomplete == 'function')
        req.oncomplete = oncomplete;
-    req.sendAsBinary(params);
-
-    info('');
-    form_clear(form);
+    if (typeof req.sendAsBinary == 'function')
+       req.sendAsBinary(params);
+    else if (typeof req.send == 'function')
+       req.send(params);
+    else
+       error('No send function available in XMLHttpRequest');
 }
 
 function info(msg)
 {
-    var status = document.getElementById('status');
-    if (!status) return;
+    var status = $('#status');
+    if (!status.length) return;
 
-    status.innerHTML = msg;
-    status.className = 'status_ok';
+    status.text(msg).attr('class', 'status_ok');
 }
 
 function error(msg)
 {
-    var status = document.getElementById('status');
-    if (!status) return;
+    var status = $('#status');
+    if (!status.length) return;
 
-    status.innerHTML = msg;
-    status.className = 'status_error';
+    status.text(msg).attr('class', 'status_error');
 }
 
 function button_enable(id)
 {
-    var button = document.getElementById(id);
-    if (!button) return;
+    var button = $('#'+id);
+    if (!button.length) return;
 
-    button.className = '';
-    button.disabled = false;
+    button.removeClass('disabled').attr('disabled', false);
 }
 
 function button_disable(id)
 {
-    var button = document.getElementById(id);
-    if (!button) return;
+    var button = $('#'+id);
+    if (!button.length) return;
 
-    button.className = 'disabled';
-    button.disabled = true;
+    button.addClass('disabled').attr('disabled', true);
 }
 
 function set_value(id, value)
@@ -220,6 +608,7 @@ function set_value(id, value)
        if (obj.type.toLowerCase() == 'checkbox') {
            if (value) obj.checked = true;
            else obj.checked = false;
+           obj.value = 'on';
        } else if (obj.type.toLowerCase() == 'file')
            return;
        else
@@ -271,35 +660,98 @@ function get_info(name, values, callback)
 /*
  * Form functions
  */
-function form_clear(form)
+function form_allow_toggle(id)
 {
-    if (!form) return;
+    $(id+' label.toggle').click(function(e){
+       var name = $(this).attr('for');
+       if (!name) return;
+       if ($('#'+name).css('display') == 'none')
+           $('#'+name).css('display', '');
+       else
+           $('#'+name).css('display', 'none');
+    });
+}
 
-    for (var i=0; i < form.childNodes.length; i++)
-       if (form.childNodes[i].nodeName.toLowerCase() == 'input'
-           && (form.childNodes[i].type.toLowerCase() == 'password' ||
-               form.childNodes[i].type.toLowerCase() == 'file'))
-           form.childNodes[i].value = '';
+function load_edit_form()
+{
+    if (Hallinta.editDialog) return;
 
-    var id = document.getElementById('edit_id');
-    if (id) id.value = '';
+    Hallinta.editDialog = new Rico.Window('Datensatz bearbeiten', {zIndex: 10});
+    Rico.addClass(Hallinta.editDialog.container, 'editDialog');
+
+    ajax_request('edit', '', load_edit_form_callback);
 }
 
-var form_first_use = true;
-function form_init()
+function load_edit_form_callback(data)
+{
+    $(Hallinta.editDialog.contentDiv).html(data.content);
+    Hallinta.editFormCheck = data.checks;
+    form_init('#form_edit');
+    form_allow_toggle('#form_edit');
+    $('#edit_id').val('');
+    button_disable('button_save');
+    button_disable('button_delete');
+
+    Hallinta.editDialog.centerPopup();
+    Hallinta.popupMobilePosition(Hallinta.editDialog);
+    Hallinta.editDialog.closePopup();
+
+    if (typeof Hallinta.postLoadForm == 'function')
+       Hallinta.postLoadForm(data);
+}
+
+function focus_first_element(name)
 {
-    var form = document.getElementById('form_edit');
+    $(name+' input:visible,'+name+' select:visible,'+name+' textarea:visible').first().focus();
+}
 
-    if (!form) return;
+function enable_file_upload(name)
+{
+    $(name+' input[type="file"]').change(function(e){
+       $(name+' input[data-for="'+$(this).attr('id')+'"]').val(e.target.files[0].name);
+    });
+    $(name+' input.file-upload').click(function(e){
+       $(name+' #'+$(this).attr('data-for')).click();
+    });
+    $(name+' input.file-upload').keyup(function(e){
+       if (e.keyCode == 32)
+           $(name+' #'+$(this).attr('data-for')).click();
+    });
+}
 
-    form_clear(form);
+function form_clear(name)
+{
+    $(name+' input,'+name+' textarea').each(function(i,e){
+       $(e).val('');
+    });
+
+    $(name+' select').each(function(i,e){
+       if ($(e).find('option').length == 2 && $(e).find('option[value=""]').length == 1) {
+           $(e).val($(e).find('option:nth-child(2)').attr('value'));
+       } else {
+           $(e).val('');
+       }
+    });
+}
 
-    var elem = form.findFirstElement();
-    if (elem) elem.activate();
+var form_first_use = true;
+function form_init(formname)
+{
+    if (!$(formname).length) return;
+
+    form_clear(formname);
+    $(formname + ' input[type="file"]').each(function(i,e){
+       $(e).change(form_file_change);
+    });
+    enable_file_upload(formname);
+    focus_first_element(formname);
 }
 
 function form_elem_error(elem,text)
 {
+    if (typeof elem == 'string')
+       elem = document.getElementById(elem);
+    if (!elem) return;
     elem.style.borderColor = 'red';
     elem.style.borderWidth = '1px';
     elem.style.borderStyle = 'solid';
@@ -308,6 +760,9 @@ function form_elem_error(elem,text)
 
 function form_elem_ok(elem)
 {
+    if (typeof elem == 'string')
+       elem = document.getElementById(elem);
+    if (!elem) return;
     elem.style.borderColor = '';
     elem.style.borderWidth = '';
     elem.style.borderStyle = '';
@@ -315,15 +770,15 @@ function form_elem_ok(elem)
 
 function form_reset_errors()
 {
-   for (name in form_check) {
-       var elem = document.getElementById('edit_' + name);
-       form_elem_ok(elem);
+    for (name in Hallinta.editFormCheck) {
+       form_elem_ok('edit_' + name);
     }
 }
 
 var form_check_regexp = {
     number: /^\d+$/,
     decimal: /^\d*([,\.]\d+)?$/,
+    time: /^(\d\d?:\d\d?(:\d\d?)?)$/,
     date: /^(\d\d?\.\d\d?\.[1-9]\d\d\d)|([1-9]\d\d\d-\d\d?-\d\d?)$/
 };
 
@@ -353,14 +808,8 @@ function form_elem_check(elem,check)
 
 function form_elem_onblur(e)
 {
-    var first_use = form_first_use;
-    form_first_use = false;
-
-    var elem = document.getElementById('form_edit').findFirstElement();
-    if (first_use && elem == e.originalTarget && !e.originalTarget.length) return;
-
-    if (form_check[e.originalTarget.name]) {
-       var error = form_elem_check(e.originalTarget, form_check[e.originalTarget.name]);
+    if (Hallinta.editFormCheck[e.originalTarget.name]) {
+       var error = form_elem_check(e.originalTarget, Hallinta.editFormCheck[e.originalTarget.name]);
        if (error.length)
            form_elem_error(e.originalTarget, error);
        else
@@ -371,9 +820,9 @@ function form_elem_onblur(e)
 function form_checks(form)
 {
     var errors = '';
-    for (name in form_check) {
+    for (name in Hallinta.editFormCheck) {
        var elem = document.getElementById('edit_' + name);
-       var error = form_elem_check(elem, form_check[name]);
+       var error = form_elem_check(elem, Hallinta.editFormCheck[name]);
        if (error.length) {
            form_elem_error(elem, error);
            errors += '. ' + error + "\n";
@@ -395,45 +844,76 @@ function select_update(id, options, empty)
 
     if (typeof empty == 'undefined') empty = 0;
 
+    if (typeof obj.options == 'undefined') return;
+
     obj.options.length = empty;
 
     for (var i=0; i < options.length; i++)
       obj.options[empty+i] = new Option(options[i].text,options[i].id,false,false);
 }
 
-var pre_save = false;
-var pre_insert = false;
-var post_save = function() { grid_update(grid); }
-var post_delete = function() { grid_update(grid); }
-
 function save_callback(data)
 {
     info('Datensatz gespeichert');
+    Hallinta.hideMsg();
+
+    if (Hallinta.grid)
+       grid_update(Hallinta.grid);
 
-    if (typeof post_save == 'function')
-       post_save();
+    if ($('#form_edit').length) {
+       $('#edit_id').val('');
+       button_disable('button_save');
+       button_disable('button_delete');
+       focus_first_element('#form_edit')
+    }
 
-    var form = document.getElementById('form_edit');
+    if (data.func == 'save') {
+       if (typeof Hallinta.postSave == 'function')
+           Hallinta.postSave(data);
 
-    if (!form) return;
+       if (Hallinta.clearDialogAfterSave)
+           form_clear('#form_edit');
 
-    form_clear(form);
-    Form.focusFirstElement(form);
+       if (Hallinta.closeDialogAfterSave)
+           Hallinta.editDialog.closePopup();
+    }
+
+    if (data.func == 'insert') {
+       if (typeof Hallinta.postInsert == 'function')
+           Hallinta.postInsert(data);
+
+       if (Hallinta.fetchItemAfterInsert) {
+           ajax_request('fetch', 'id=' + data.id, fetch_callback);
+            form_reset_errors();
+       }
+
+       if (Hallinta.clearDialogAfterInsert)
+           form_clear('#form_edit');
+
+       if (Hallinta.closeDialogAfterInsert)
+           Hallinta.editDialog.closePopup();
+    }
+
+    button_enable('button_insert');
 }
 
 function delete_callback(data)
 {
     info('Datensatz gelöscht');
 
-    if (typeof post_delete == 'function')
-       post_delete();
+    if (Hallinta.grid)
+       grid_update(Hallinta.grid);
+
+    if (Hallinta.closeDialogAfterDelete)
+       Hallinta.editDialog.closePopup();
+
+    if (typeof Hallinta.postDelete == 'function')
+       Hallinta.postDelete(data);
 }
 
 function form_save(obj)
 {
-    var id = document.getElementById('edit_id');
-
-    if (!id.value.length)
+    if (!$('#edit_id').val().length)
        return form_insert(obj);
 
     if (!form_checks(obj.form))
@@ -441,12 +921,12 @@ function form_save(obj)
 
     info('');
 
-    if (typeof pre_save == 'function')
-       if (!pre_save())
+    if (typeof Hallinta.preSave == 'function')
+       if (!Hallinta.preSave())
            return false;
 
-
-    ajax_request('save', Form.serialize(obj.form), save_callback);
+    Hallinta.showMsg('Die Daten werden gespeichert...');
+    ajax_request('save', $('#form_edit').serialize(), save_callback);
     return false;
 }
 
@@ -457,11 +937,21 @@ function form_insert(obj)
 
     info('');
 
-    if (typeof pre_insert == 'function')
-       if (!pre_insert())
+    button_disable('button_insert');
+    if (typeof Hallinta.preInsert == 'function')
+       if (!Hallinta.preInsert()) {
+           button_enable('button_insert');
            return false;
+       }
+
+    Hallinta.showMsg('Die Daten werden gespeichert...');
+    ajax_form_submit('insert', '#form_edit', save_callback);
+
+    info('');
+    $('#edit_id').val('');
+    button_disable('button_save');
+    button_disable('button_delete');
 
-    ajax_form_submit('insert', obj.form, save_callback);
     return false;
 }
 
@@ -471,10 +961,12 @@ function form_delete(obj)
 
     if (!id.value.length) return false;
 
-    var source = document.getElementById('edit_source');
+    if (typeof Hallinta.preDelete == 'function')
+       if (!Hallinta.preDelete())
+           return false;
+
     info('');
-    var params = 'id='+id.value + '&source='+source.value;
-    ajax_request('delete', params, delete_callback);
+    ajax_request('delete', 'id='+id.value, delete_callback);
     return false;
 }
 
@@ -489,14 +981,45 @@ function details_callback(data)
 
 function fetch_callback(data)
 {
-    for (var id in data)
-       set_value('edit_'+id, data[id]);
+    for (var id in data) {
+       if (data[id] === null)
+           set_value('edit_'+id, '');
+       else
+           set_value('edit_'+id, data[id]);
+    }
 
-    var status = document.getElementById('form_status');
-    status.innerHTML = 'Geändert: ' + data.sys_edit + ' von ' + data.sys_user;
+    $('#form_status').text(data.sys_edit + ' (' + data.sys_user + ')');
+    button_enable('button_save');
+    button_enable('button_delete');
 
-    var form = document.getElementById('form_edit');
-    Form.focusFirstElement(form);
+    Hallinta.editDialog.openPopup();
+    focus_first_element('#form_edit');
+
+    if (typeof Hallinta.postFetch == 'function')
+       Hallinta.postFetch(data);
+}
+
+/*
+ * Template functions
+ */
+function load_template(name, payload, callback)
+{
+    if (payload === null || (typeof payload == 'string' && payload == '')) {
+       payload ='template=' + name;
+    } else if (typeof payload == 'string') {
+       payload +='&template=' + name;
+    } else if (typeof payload == 'object') {
+       payload['template'] = name;
+    } else {
+       error('Template '+name+' error');
+       return;
+    }
+
+    ajax_request('template', payload, function(data){
+       if (typeof callback == 'function') {
+           callback(name, data);
+       }
+    });
 }
 
 /*
@@ -504,7 +1027,11 @@ function fetch_callback(data)
  */
 function gridDrillDown(e)
 {
-    if (e.originalTarget && e.originalTarget.target && e.originalTarget.target == '_top')
+    if (e.target && !$(e.target).hasClass('ricoLG_cell') &&
+       !(e.target.id && e.target.id.search('_desc_') != -1))
+       return
+
+    if (e.originalTarget && e.originalTarget.target && e.originalTarget.target == '_new')
        return;
 
     if (e.originalTarget && 
@@ -512,37 +1039,35 @@ function gridDrillDown(e)
        (!e.originalTarget.className || (e.originalTarget.className.split('_'))[0] !== 'ricoLG'))
        return;
 
+    Hallinta.mainId = false;
     var id = 0; // Column 0 contains ID
-    var row = grid.edit.drillDown(e,0,0);
-    var cell = grid.columns[id].cell(row);
+    var row = Hallinta.grid.edit.drillDown(e,0,0);
+    var cell = Hallinta.grid.columns[id].cell(row);
     if (!cell) return;
     var value = cell.innerHTML;
 
     if (!value.length || value == '&nbsp;') return;
+    Hallinta.mainId = value;
 
     if (document.getElementById('details')) {
-       var params = 'source=' + grid.tableId.substr(5) + '&id=' + value;
+       var params = 'source=' + Hallinta.grid.tableId.substr(5) + '&id=' + Hallinta.mainId;
        ajax_request('details', params, details_callback);
     }
 
-    if (document.getElementById('form_edit')) {
-       var status = document.getElementById('form_status');
-       if (status.style.display == '') {
-           var params = 'source=' + grid.tableId.substr(5) + '&id=' + value;
-           ajax_request('fetch', params, fetch_callback);
-           form_reset_errors();
-
-           if (edit_hidden) edit_show();
-       }
+    if (Hallinta.canEdit) {
+       ajax_request('fetch', 'id=' + Hallinta.mainId, fetch_callback);
+        form_reset_errors();
     }
 
-    if (second_visible()) {
-       second.buffer.options.requestParameters = ['second_id=' + value];
-       grid_update(second);
+    for (var id in Hallinta.seconds) {
+       if (Hallinta.seconds[id].popup.visible()) {
+           Hallinta.seconds[id].grid.buffer.options.requestParameters = ['second_id=' + Hallinta.mainId];
+           grid_update(Hallinta.seconds[id].grid);
+       }
     }
 
-    if (typeof drilldown == 'function')
-       drilldown(e,value);
+    if (typeof Hallinta.drillDown == 'function')
+       Hallinta.drillDown(e, Hallinta.mainId);
 }
 
 var grid_offset = 0;
@@ -553,6 +1078,7 @@ function gridOnScroll(grid, offset)
     var max = Math.min(offset+grid.pageSize, grid.buffer.totalRows);
     var info = document.getElementById('info_' + grid.tableId);
     info.innerHTML = 'Datensatz ' + (offset+1) + ' - ' + max + ' von ' + grid.buffer.totalRows;
+    reset_logout();
 }
 
 /* Update an existing grid
@@ -594,7 +1120,7 @@ function grid_update_filters(grid)
        $(grid.filterId(c)).options.length = 1;
 
        var options = {};
-       Object.extend(options, grid.buffer.ajaxOptions);
+       Rico.extend(options, grid.buffer.ajaxOptions);
        var colnum = typeof(fmt.filterCol)=='number' ? fmt.filterCol : c;
 
        options.parameters = 'id='+grid.tableId+'&distinct='+colnum;
@@ -612,7 +1138,7 @@ function grid_update_filters(grid)
        }
 
        options.onComplete = grid.filterValuesUpdate.bind(grid,c);
-       new Ajax.Request(grid.buffer.dataSource, options);
+       new Rico.Request(grid.buffer.dataSource, options);
     }
 }
 
@@ -620,17 +1146,15 @@ var grid_column_edit = new Array();
 
 function grid_cell_save(id,col,value)
 {
-    var source = document.getElementById('source');
-
-    if (!source) return false;
-
-    var parms = 'source=' + source.innerHTML;
-    parms += '&callback=cellsave';
+    var parms = 'callback=cellsave';
     parms += '&id=' + id;
     parms += '&column=' + col;
     parms += '&value=' + value;
 
-    ajax_request('function', parms, false);
+    ajax_request('function', parms, function(data){
+       if (typeof Hallinta.gridMenuColumns['main'][col]['callback'] == 'function')
+           Hallinta.gridMenuColumns['main'][col]['callback'](data);
+    });
 }
 
 function grid_cell_value()
@@ -657,46 +1181,53 @@ function grid_cell_value()
     grid_cell_save(this.grid.buffer.getCell(bufRow,0), this.col, this.value);
 }
 
-function grid_dataMenuHandler(grid,row,col,onBlankRow)
-{
-    var default_menu = true;
+Hallinta.dataMenuHandler = function(grid,row,col,onBlankRow) {
+    var defaultMenu = true;
 
-    if (grid_column_edit[col] &&
-       typeof grid_column_edit[col].default_menu !== 'undefined' &&
-       grid_column_edit[col].default_menu == false)
-       default_menu = false;
+    if (grid == Hallinta.grid)
+       var name = 'main';
+    else {
+       var name =  grid.tableId.substring(Hallinta.grid.tableId.length+2).split('__')[0];
+    }
 
-    if (default_menu && typeof grid.menu.options.dataMenuHandlerOriginal == 'function') {
+    if (typeof Hallinta.gridMenuColumns[name] == 'object' &&
+       typeof Hallinta.gridMenuColumns[name][col] == 'object' &&
+       typeof Hallinta.gridMenuColumns[name][col].defaultMenu != 'undefined' &&
+       Hallinta.gridMenuColumns[name][col].defaultMenu == false)
+       defaultMenu = false;
+
+    if (defaultMenu && typeof grid.menu.options.dataMenuHandlerOriginal == 'function') {
        grid.menu.options.dataMenuHandlerOriginal(grid,row,col,onBlankRow);
-       grid.menu.div.style.width = '17em';
+       grid.menu.content.style.width = '17em';
     }
 
-    if (grid_column_edit[col]) {
+    if (typeof Hallinta.gridMenuColumns[name] == 'object' &&
+        typeof Hallinta.gridMenuColumns[name][col] == 'object') {
        var submenu;
-       if (default_menu) {
-           var submenu = new Rico.Menu(grid_column_edit[col].width);
+       if (defaultMenu) {
+           submenu = new Rico.Menu(Hallinta.gridMenuColumns[name][col].width);
            submenu.createDiv();
        } else
            submenu = grid.menu;
 
-       for (i=0; i < grid_column_edit[col].values.length; i++)
-           submenu.addMenuItem(grid_column_edit[col].values[i][0],
+       for (var i=0; i < Hallinta.gridMenuColumns[name][col].values.length; i++)
+           submenu.addMenuItem(Hallinta.gridMenuColumns[name][col].values[i][0],
                                grid_cell_value.bind({grid: grid, row: row, col: col,
-                                           visible: grid_column_edit[col].values[i][1],
-                                           value: grid_column_edit[col].values[i][2]}));
+                                                     visible: Hallinta.gridMenuColumns[name][col].values[i][1],
+                                                     value: Hallinta.gridMenuColumns[name][col].values[i][2]}));
 
-       if (!default_menu && typeof grid_column_edit[col].width !== 'undefined')
-           grid.menu.div.style.width = grid_column_edit[col].width;
+       if (!defaultMenu && typeof Hallinta.gridMenuColumns[name][col].width !== 'undefined')
+           grid.menu.content.style.width = Hallinta.gridMenuColumns[name][col].width;
 
-       if (default_menu)
+       if (defaultMenu)
            grid.menu.addSubMenuItem('Wert setzen', submenu, true);
     }
 
-    if (default_menu)
+    if (defaultMenu)
        return true;
     else
        return false;
-}
+};
 
 var calendars = new Array();
 function calendar_callback(value)
@@ -713,84 +1244,56 @@ function calendar(name,event)
         calendars[name] = new Rico.CalendarControl('calendar_'+name,
                                                   {startAt: 1,
                                                    dateFmt: 'dd.mm.yyyy',
+                                                   zIndex: 20,
                                                    showWeekNumber: 1});
-        calendars[name].atLoad();
         calendars[name].returnValue = calendar_callback;
-        RicoUtil.positionCtlOverIcon(calendars[name].container,input);
+        Rico.positionCtlOverIcon(calendars[name].container,input);
         calendars[name].open(input.value);
        calendars[name].input = input;
     } else {
-        if (Element.visible(calendars[name].container))
+       if ($('#calendar_'+name+':visible').length) {
             calendars[name].close();
-        else
+       } else {
+            Rico.positionCtlOverIcon(calendars[name].container,input);
             calendars[name].open(input.value);
+       }
     }
-    Event.stop(event);
 }
 
-function edit_hide()
-{
-    var col_edit = document.getElementById("column_edit");
-    var col_grid = document.getElementById("column_grid");
-    col_edit.style.display = 'none';
-    col_grid.style.width = '100%';
-
-    edit_hidden = true;
-
-    grid.sizeDivs();
-    grid.resizeWindow();
-}
-
-function edit_show()
+/*
+ * Menu
+ */
+function display_menu(event, base)
 {
-    var col_edit = document.getElementById("column_edit");
-    var col_grid = document.getElementById("column_grid");
-    col_edit.style.display = '';
-    col_grid.style.width = '';
-
-    edit_hidden = false;
-
-    grid.sizeDivs();
-    grid.resizeWindow();
-}
+    if (!Hallinta.menuDialog) {
+       Hallinta.menuDialog = new Rico.Window('Menü', {width: 'auto', zIndex: 100});
+       Rico.addClass(Hallinta.menuDialog.container, 'menuDialog');
+    }
 
-function resize_grids()
-{
-    var div_grid = document.getElementById('div_grid');
-    var info = document.getElementById('info_'+second.tableId);
-    var height = RicoUtil.windowHeight() - (info.offsetTop - div_grid.offsetTop) - 60;
-    div_grid.style.height = height + 'px';
-    grid.resizeWindow();
-}
+    var page = '';
+    if (typeof base == 'undefined') {
+       base = Hallinta.menuBase;
+       if (Hallinta.pageSource != 'start')
+           page = Hallinta.pageSource;
+    }
 
-function second_visible()
-{
-    if (typeof second == 'undefined') return false;
-    var div_second = document.getElementById('second');
-    return div_second.style.display == '';
+    ajax_request('menu', 'base='+base+'&page='+page, display_menu_callback);
+    return false;
 }
 
-var div_grid_height = '';
-function second_toggle()
+function display_menu_callback(data)
 {
-    var div_grid = document.getElementById('div_grid');
-    var div_second = document.getElementById('second');
-    var toggle_icon = document.getElementById('icon_toggle');
+    var centerDialog = true;
+    if ($(Hallinta.menuDialog.contentDiv).text().length)
+       centerDialog = false;
 
-    if (second_visible()) {
-       div_grid_height = div_grid.style.height;
-       div_second.style.display = 'none';
+    $(Hallinta.menuDialog.titleDiv).find('.ricoTitleSpan').text(data.title);
+    $(Hallinta.menuDialog.contentDiv).html(data.menu);
 
-       var height = RicoUtil.windowHeight() - div_grid.offsetTop - 10;
-       div_grid.style.height = height + 'px';
-       grid.resizeWindow();
-       toggle_icon.style.display = '';
-    } else {
-       div_grid.style.height = div_grid_height;
-       div_second.style.display = '';
-       grid.resizeWindow();
-       second.clearRows();
-       second.resizeWindow();
-       toggle_icon.style.display = 'none';
-    }
+    if (centerDialog) {
+       var pos = $('#btn_menu').offset();
+       Hallinta.menuDialog.openPopup(pos.left,pos.top+20);
+       Hallinta.menuDialog.openPopup(pos.left-5-$(Hallinta.menuDialog.contentDiv).width(),pos.top+20);
+    } else
+       Hallinta.menuDialog.openPopup();
 }
index 8c239ce..81a02fe 100644 (file)
@@ -2,41 +2,6 @@
 
 require_once('db.php');
 
-function __autoload($class)
-{
-  $found = false;
-
-  if (empty($_GLOBALS['module'])) {
-    if (!empty($_REQUEST['source'])) $name = sanitise_filename($_REQUEST['source']);
-    if (!empty($_GET['msak'])) $name = sanitise_filename($_GET['mask']);
-    list($module,$fname) = explode('__', $name);
-    if (!empty($module)) $_GLOBALS['module'] = $module;
-  }
-
-  if (!$found &&
-      !empty($_REQUEST['source']) &&
-      !empty($_GLOBALS['module']) &&
-      is_dir($_SESSION['sys']['basedir'].'masks/'.$_GLOBALS['module'].'/class') &&
-      file_exists($_SESSION['sys']['basedir'].'masks/'.$_GLOBALS['module'].'/class/'.strtolower($class).'.class.php')) {
-    include_once($_SESSION['sys']['basedir'].'masks/'.$_GLOBALS['module'].'/class/'.strtolower($class).'.class.php');
-    $found = true;
-  }
-
-  if (!$found &&
-      is_dir($_SESSION['sys']['basedir'].'class') &&
-      file_exists($_SESSION['sys']['basedir'].'class/'.strtolower($class).'.class.php')) {
-    include_once($_SESSION['sys']['basedir'].'class/'.strtolower($class).'.class.php');
-    $found = true;
-  }
-
-  if (!$found) {
-    $text = sprintf('Class %s not found%',
-                 $class,
-                 defined(MODULNAME) ? ' in module ' . MODULNAME : '');
-    die($text);
-  }
-}
-
 function passwd($login,$pass)
 {
   return md5(md5($pass).$login);
@@ -45,10 +10,23 @@ function passwd($login,$pass)
 function format_ajax($data)
 {
   header('Content-type: application/json; charset=UTF-8');
-  echo json_encode($data);
+
+  $return = json_encode($data);
+
+  if ($return === false) {
+    error_log('Return ' . var_export($data,true));
+    $return = json_encode(array('status' => false, 'error' => 'Rückgabedaten können nicht kodiert werden.'));
+  }
+
+  echo $return;
   exit;
 }
 
+function ajax_error($text)
+{
+  format_ajax(array('error' => $text));
+}
+
 function format_xml($errmsg)
 {
   header("Cache-Control: no-cache");
@@ -65,32 +43,48 @@ function format_xml($errmsg)
   exit;
 }
 
-function check_permissions($name)
+function check_permissions()
 {
-  global $db;
+    $menu = new MenuItem();
 
-  $parts = explode('__', $name);
-  $form = $parts[0] . '|' . $parts[1];
+    if (!$menu->hasPermission()) {
+       $hallinta = Hallinta::instance();
+       error_log(sprintf('Unauthorised access to %s/%s', $hallinta->module(), $hallinta->page()));
+       return false;
+    }
 
-  $sql = sprintf("SELECT count(*) AS count FROM sys_mask "
-                . "JOIN sys_menu ON sys_mask.menu = sys_menu.id "
-                . "JOIN sys_group_mask ON sys_mask.id = sys_group_mask.mask "
-                . "WHERE sys_group_mask.gid = %d AND fname = %s",
-                $_SESSION['sys']['group'], $db->quote($form));
+  return true;
+}
 
-  $count = $db->fetchValue($sql);
+function check_empty_session()
+{
+  if (!empty($_SESSION['sys']['login'])) return;
 
-  if ($count === false) {
-    error_log('Unauthorised access to ' . $form);
-    return false;
-  }
+  if (substr($_SERVER["SCRIPT_FILENAME"],-10) == '/index.php' &&
+      !empty($_POST['login']) && !empty($_POST['passwd']))
+    return;
 
-  if ($count == 0) {
-    error_log('Unauthorised access to ' . $form);
-    return false;
-  }
+  if (isset($_GET['login']) && $_GET['login'] == 'true') return;
 
-  return true;
+  /* table data */
+  if (substr($_SERVER["SCRIPT_FILENAME"],-17) == '/ricoXMLquery.php' &&
+      !empty($_GET['id']) && substr($_GET['id'],0,5) == 'grid_')
+    format_xml("No permission to access data.\nNo active session found.\nYou may need to re-login.");
+
+  /* table connections */
+  if (substr($_SERVER["SCRIPT_FILENAME"],-25) == '/ricoUpdateConnection.php' &&
+      !empty($_GET['id']) && substr($_GET['id'],0,5) == 'grid_')
+    format_ajax(array('error' => 'No permission to access data'));
+
+  /* ajax calls */
+  if (substr($_SERVER["SCRIPT_FILENAME"],-9) == '/ajax.php' &&
+      !empty($_POST['source']))
+    format_ajax(array('error' => "No permission to access data.\nNo active session found.\nYou need to re-login.",
+                     'logout' => true));
+
+    header(sprintf('Location: %s?login=true', Hallinta::instance()->urlbase()));
+    echo "No permission to access data.\nNo active session found.\nYou need to re-login.";
+    exit();
 }
 
 function check_session()
@@ -98,7 +92,7 @@ function check_session()
   /* table data */
   if (substr($_SERVER["SCRIPT_FILENAME"],-17) == '/ricoXMLquery.php' &&
       !empty($_GET['id']) && substr($_GET['id'],0,5) == 'grid_') {
-    if (check_permissions(substr($_GET['id'],5)))
+    if (check_permissions())
       return true;
     else {
       if (empty($_SESSION['sys']['login']))
@@ -111,7 +105,7 @@ function check_session()
   /* table connections */
   if (substr($_SERVER["SCRIPT_FILENAME"],-25) == '/ricoUpdateConnection.php' &&
       !empty($_GET['id']) && substr($_GET['id'],0,5) == 'grid_') {
-    if (check_permissions(substr($_GET['id'],5)))
+    if (check_permissions())
       return true;
     else
       format_ajax(array('error' => 'No permission to access data'));
@@ -119,8 +113,9 @@ function check_session()
 
   /* ajax calls */
   if (substr($_SERVER["SCRIPT_FILENAME"],-9) == '/ajax.php' &&
+      $_POST['func'] != 'menu' &&
       !empty($_POST['source'])) {
-    if (check_permissions($_POST['source']))
+    if (check_permissions())
       return true;
     else {
       if (empty($_SESSION['sys']['login']))
@@ -132,8 +127,11 @@ function check_session()
   }
 
   if (!empty($_SESSION['sys']['login']) && !empty($_GET['logout'])) {
+    if (empty($_GET['auto']))
+       clear_page_log();
+    $_SESSION = array();
     session_destroy();
-    header('Location: ./?login=true');
+    header(sprintf('Location: %s?login=true', Hallinta::instance()->urlbase()));
     exit();
   }
 
@@ -141,19 +139,25 @@ function check_session()
       !empty($_POST['login']) && !empty($_POST['passwd'])) {
     require_once('lib/login.php');
     if (check_passwd()) {
-      header('Location: ./');
+       if (strlen($_SESSION['sys']['page'])) {
+           $parts = explode('__', $_SESSION['sys']['page']);
+           $url = sprintf("%s%s/%s", Hallinta::instance()->urlbase(), $parts[0], $parts[1]);
+       } else {
+           $url = Hallinta::instance()->urlbase();
+       }
+       header(sprintf('Location: %s', $url));
       exit();
     }
   }
 
   if (empty($_SESSION['sys']['login']) && empty($_GET['login'])) {
-    header('Location: ./?login=true');
+    header(sprintf('Location: %s?login=true', Hallinta::instance()->urlbase()));
     exit();
   }
 
   /* regular mask */
   if (!empty($_GET['mask'])) {
-    if (check_permissions($_GET['mask']))
+    if (check_permissions())
       return true;
     else {
       header('Location: ./');
@@ -169,17 +173,17 @@ function sanitise_filename($file)
   return str_replace('./','x',$file);
 }
 
-function load_mask($name)
+function load_mask($module, $page)
 {
   global $mask;
-  global $jscode;
-  global $style;
+  global $javascript;
+  global $styles;
 
-  $name = sanitise_filename($name);
-  list($module,$fname) = explode('__', $name);
   $_SESSION['module'] = $module;
+  $_SESSION['mask'] = $page;
 
-  $file = $_SESSION['sys']['basedir'] . 'masks/' . $module . '/' . $fname . '.php';
+  $file = sprintf('%s%s/%s/%s.php', $_SESSION['sys']['basedir'], HALLINTA_MODULEDIR,
+                 $module, sanitise_filename($page));
 
   if (!file_exists($file))
     return false;
@@ -189,47 +193,113 @@ function load_mask($name)
   return true;
 }
 
-function load_js($jsfiles, $jscode)
+function grid_definition($id)
 {
-  $ret = '';
-  foreach ($jsfiles as $file)
-    $ret .= sprintf('<script type="text/javascript" src="%s"></script>'."\n", $file);
-
-  if (!empty($jscode))
-    $ret .= sprintf('<script type="text/javascript">'."\n%s\n</script>\n", implode("\n",$jscode));
+  global $mask;
+  $hallinta = Hallinta::instance();
+  $maskName = substr($id,5);
+  $secondName = false;
+
+  if (substr($maskName, -8) == '__second') {
+    $maskName = substr($maskName,0,-8);
+    $secondName = substr($maskName,strrpos($maskName, '__')+2);
+    $maskName = substr($maskName,0,strrpos($maskName, '__'));
+  }
 
-  return $ret;
+  load_mask($hallinta->module(), $hallinta->page());
+  if ($secondName)
+    return $mask['second'][$secondName];
+  return $mask;
 }
 
-function load_style($style)
+function load_start()
 {
-  $ret = '';
-  if (!empty($style))
-    $ret .= sprintf('<style type="text/css">'."\n%s\n</style>\n", implode("\n",$style));
+  global $styles;
+  global $javascript;
 
-  return $ret;
+  $styles->file('lib/rico3/ricoClient/css/rico.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui_hallinta.css');
+
+  $javascript->file('lib/rico3/ricoClient/js/rico2jqu.js');
+  $javascript->file('lib/rico3/minsrc/rico.js');
+  $javascript->file('lib/rico3/minsrc/ricoLocale_en.js');
+  $javascript->file('lib/rico3/minsrc/ricoUI.js');
+  $javascript->file('lib/rico3/ricoClient/js/ricoThemeroller.js');
+  $javascript->add('Hallinta.pageSource = "start";');
+  $javascript->add(sprintf('Hallinta.isMobile = %s;', Hallinta::instance()->isMobile()?'true':'false'));
+
+  $ret = '';
+  $fname = $_SESSION['sys']['basedir'] . 'data/misc/start.js';
+  if (file_exists($fname))
+    $javascript->onLoad(file_get_contents($fname));
+  $fname = $_SESSION['sys']['basedir'] . 'data/misc/start.html';
+  if (file_exists($fname))
+    return file_get_contents($fname);
 }
 
 function process()
 {
+  $hallinta = Hallinta::instance();
+
   if (!empty($_GET['login'])) {
     require_once('lib/login.php');
     return mask_login();
   }
 
-  if (!empty($_GET['mask'])) {
-    require_once('lib/mask.php');
-    return mask($_GET['mask']);
+  Javascript::instance()->file('lib/functions.js');
+  Actions::instance()->addLink(new Link(array('id' => 'btn_menu',
+                                             'icon' => $hallinta->urlbase().'images/icons/menu.png',
+                                             'title' => 'Menü',
+                                             'function' => 'display_menu')));
+
+  $module = $hallinta->module();
+  if (!is_null($module)) {
+      if ($hallinta->isMobile()) {
+         Actions::instance()->addLink(new Link(array('id' => 'btn_columns',
+                                                     'icon' => $hallinta->urlbase().'images/icons/liste.gif',
+                                                     'title' => 'Spalten',
+                                                     'function' => 'Hallinta.grid.chooseColumns')));
+      }
+
+      save_page_log();
+      require_once('lib/mask.php');
+      return mask($module, $hallinta->page());
   }
 
-  $ret = '';
-  $fname = $_SESSION['sys']['basedir'] . 'data/misc/start.html';
-  if (file_exists($fname))
-    return file_get_contents($fname);
-  
+  $ret = load_start();
   return $ret;
 }
 
+$debug_file = false;
+function debug($name, $text=false)
+{
+  global $debug_file;
+
+  if (!$debug_file) {
+    $dir = $_SESSION['sys']['basedir'].'/archive/debug';
+    if (!is_dir($dir))
+      if (!@mkdir($path, 0770))
+       return;
+
+    $debug_file = fopen($dir.'/debug.log', 'a');
+  }
+
+  if ($text === false) {
+    $text = $name;
+    $name = false;
+  }
+
+  if (!is_string($text))
+    $text = var_export($text,true);
+
+  if ($debug_file) {
+    fputs($debug_file, sprintf("%s %s: %s%s\n", date('Y-m-d'), $_SESSION['sys']['login'],
+                              $name === false ? '' : $name . ' = ',
+                              $text));
+  }
+}
+
 function debug_log($text)
 {
   global $debug_info;
@@ -268,18 +338,22 @@ function grid_sql_join($mask)
   return $ret;
 }
 
-function grid_lookup_sql($table,$id,$text)
+function grid_lookup_sql($table,$id,$text,$map=false)
 {
   global $db;
 
-  if (empty($_GET['mask'])) return;
+  if (empty($_POST['func'])) return;
+  if ($_POST['func'] != 'grid') return;
 
-  $sql = "SELECT $id,$text FROM $table ORDER BY $text";
+  $sql = "SELECT $id,$text AS text FROM $table ORDER BY text";
   $sth = $db->query($sql);
 
   $result = array();
+  if (is_array($map))
+    foreach ($map as $k => $v)
+      $result[] = sprintf("%d: '%s'", $k, $v);
   while ($row = $sth->fetch())
-    $result[] = sprintf("%d: '%s'", $row[$id], $row[$text]);
+    $result[] = sprintf("%d: '%s'", $row[$id], $row['text']);
 
   return '{' . implode(', ', $result) . '}';
 }
@@ -304,4 +378,45 @@ function grid_sql($name, $mask)
   if (array_key_exists('where', $mask)) $_SESSION['grid_' . $name] .= ' WHERE ' . $mask['where'];
 }
 
-?>
+function date_iso2german($date)
+{
+    if (empty($date) || $date=='0000-00-00') return "";
+    list($jahr, $monat, $tag) = explode("-", $date);
+    if ($jahr<100) $jahr += 2000;
+
+    return sprintf("%02d.%02d.%04d", $tag, $monat, $jahr);
+}
+
+function date_german2iso($date)
+{
+    if ($date=='00.00.0000') return "";
+    list($tag, $monat, $jahr) = explode(".", $date);
+    if ($jahr<100) $jahr += 2000;
+
+    return sprintf("%04d-%02d-%02d", $jahr, $monat, $tag);
+}
+
+function save_page_log()
+{
+    global $db;
+
+    $hallinta = Hallinta::instance();
+
+    $menu = new MenuItem();
+    if ($menu->isRegular()) {
+       $sql = sprintf("UPDATE sys_user SET page = '%s__%s' WHERE id = %d",
+                      $hallinta->module(), $hallinta->page(),
+                      $_SESSION['sys']['uid']);
+
+       $db->query($sql);
+    }
+}
+
+function clear_page_log()
+{
+    global $db;
+
+    $sql = sprintf("UPDATE sys_user SET page = NULL WHERE id = %d", $_SESSION['sys']['uid']);
+
+    $db->query($sql);
+}
\ No newline at end of file
diff --git a/lib/jquery-2.1.0.min.js b/lib/jquery-2.1.0.min.js
new file mode 100644 (file)
index 0000000..cbe6abe
--- /dev/null
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m=a.document,n="2.1.0",o=function(a,b){return new o.fn.init(a,b)},p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};o.fn=o.prototype={jquery:n,constructor:o,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=o.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return o.each(this,a,b)},map:function(a){return this.pushStack(o.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},o.extend=o.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||o.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(o.isPlainObject(d)||(e=o.isArray(d)))?(e?(e=!1,f=c&&o.isArray(c)?c:[]):f=c&&o.isPlainObject(c)?c:{},g[b]=o.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},o.extend({expando:"jQuery"+(n+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===o.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isPlainObject:function(a){if("object"!==o.type(a)||a.nodeType||o.isWindow(a))return!1;try{if(a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(b){return!1}return!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=o.trim(a),a&&(1===a.indexOf("use strict")?(b=m.createElement("script"),b.text=a,m.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":k.call(a)},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?o.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),o.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||o.guid++,f):void 0},now:Date.now,support:l}),o.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=o.type(a);return"function"===c||o.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="<select t=''><option selected=''></option></select>",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=jb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=kb(b);function nb(){}nb.prototype=d.filters=d.pseudos,d.setFilters=new nb;function ob(a,b){var c,e,f,g,h,i,j,k=x[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=Q.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?db.error(a):x(a,i).slice(0)}function pb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);o.find=t,o.expr=t.selectors,o.expr[":"]=o.expr.pseudos,o.unique=t.uniqueSort,o.text=t.getText,o.isXMLDoc=t.isXML,o.contains=t.contains;var u=o.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(o.isFunction(b))return o.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return o.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return o.filter(b,a,c);b=o.filter(b,a)}return o.grep(a,function(a){return g.call(b,a)>=0!==c})}o.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?o.find.matchesSelector(d,a)?[d]:[]:o.find.matches(a,o.grep(b,function(a){return 1===a.nodeType}))},o.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(o(a).filter(function(){for(b=0;c>b;b++)if(o.contains(e[b],this))return!0}));for(b=0;c>b;b++)o.find(a,e[b],d);return d=this.pushStack(c>1?o.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?o(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=o.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof o?b[0]:b,o.merge(this,o.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:m,!0)),v.test(c[1])&&o.isPlainObject(b))for(c in b)o.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=m.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=m,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):o.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(o):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),o.makeArray(a,this))};A.prototype=o.fn,y=o(m);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};o.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&o(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),o.fn.extend({has:function(a){var b=o(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(o.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?o(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&o.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?o.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(o(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(o.unique(o.merge(this.get(),o(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}o.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return o.dir(a,"parentNode")},parentsUntil:function(a,b,c){return o.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return o.dir(a,"nextSibling")},prevAll:function(a){return o.dir(a,"previousSibling")},nextUntil:function(a,b,c){return o.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return o.dir(a,"previousSibling",c)},siblings:function(a){return o.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return o.sibling(a.firstChild)},contents:function(a){return a.contentDocument||o.merge([],a.childNodes)}},function(a,b){o.fn[a]=function(c,d){var e=o.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=o.filter(d,e)),this.length>1&&(C[a]||o.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return o.each(a.match(E)||[],function(a,c){b[c]=!0}),b}o.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):o.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){o.each(b,function(b,c){var d=o.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&o.each(arguments,function(a,b){var c;while((c=o.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?o.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},o.extend({Deferred:function(a){var b=[["resolve","done",o.Callbacks("once memory"),"resolved"],["reject","fail",o.Callbacks("once memory"),"rejected"],["notify","progress",o.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return o.Deferred(function(c){o.each(b,function(b,f){var g=o.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&o.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?o.extend(a,d):d}},e={};return d.pipe=d.then,o.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&o.isFunction(a.promise)?e:0,g=1===f?a:o.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&o.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;o.fn.ready=function(a){return o.ready.promise().done(a),this},o.extend({isReady:!1,readyWait:1,holdReady:function(a){a?o.readyWait++:o.ready(!0)},ready:function(a){(a===!0?--o.readyWait:o.isReady)||(o.isReady=!0,a!==!0&&--o.readyWait>0||(H.resolveWith(m,[o]),o.fn.trigger&&o(m).trigger("ready").off("ready")))}});function I(){m.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),o.ready()}o.ready.promise=function(b){return H||(H=o.Deferred(),"complete"===m.readyState?setTimeout(o.ready):(m.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},o.ready.promise();var J=o.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===o.type(c)){e=!0;for(h in c)o.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,o.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(o(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};o.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=o.expando+Math.random()}K.uid=1,K.accepts=o.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,o.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(o.isEmptyObject(f))o.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,o.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{o.isArray(b)?d=b.concat(b.map(o.camelCase)):(e=o.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!o.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?o.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}o.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),o.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;
+while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=o.camelCase(d.slice(5)),P(f,d,e[d]));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=o.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),o.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||o.isArray(c)?d=L.access(a,b,o.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=o.queue(a,b),d=c.length,e=c.shift(),f=o._queueHooks(a,b),g=function(){o.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:o.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),o.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?o.queue(this[0],a):void 0===b?this:this.each(function(){var c=o.queue(this,a,b);o._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&o.dequeue(this,a)})},dequeue:function(a){return this.each(function(){o.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=o.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===o.css(a,"display")||!o.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=m.createDocumentFragment(),b=a.appendChild(m.createElement("div"));b.innerHTML="<input type='radio' checked='checked' name='t'/>",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";l.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return m.activeElement}catch(a){}}o.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=o.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof o!==U&&o.event.triggered!==b.type?o.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],n=q=h[1],p=(h[2]||"").split(".").sort(),n&&(l=o.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=o.event.special[n]||{},k=o.extend({type:n,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&o.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(n,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),o.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],n=q=h[1],p=(h[2]||"").split(".").sort(),n){l=o.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||o.removeEvent(a,n,r.handle),delete i[n])}else for(n in i)o.event.remove(a,n+b[j],c,d,!0);o.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,p=[d||m],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||m,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+o.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[o.expando]?b:new o.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:o.makeArray(c,[b]),n=o.event.special[q]||{},e||!n.trigger||n.trigger.apply(d,c)!==!1)){if(!e&&!n.noBubble&&!o.isWindow(d)){for(i=n.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||m)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:n.bindType||q,l=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),l&&l.apply(g,c),l=k&&g[k],l&&l.apply&&o.acceptData(g)&&(b.result=l.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||n._default&&n._default.apply(p.pop(),c)!==!1||!o.acceptData(d)||k&&o.isFunction(d[q])&&!o.isWindow(d)&&(h=d[k],h&&(d[k]=null),o.event.triggered=q,d[q](),o.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=o.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=o.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=o.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((o.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?o(e,this).index(i)>=0:o.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||m,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[o.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new o.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=m),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&o.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return o.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=o.extend(new o.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?o.event.trigger(e,null,b):o.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},o.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},o.Event=function(a,b){return this instanceof o.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.getPreventDefault&&a.getPreventDefault()?Z:$):this.type=a,b&&o.extend(this,b),this.timeStamp=a&&a.timeStamp||o.now(),void(this[o.expando]=!0)):new o.Event(a,b)},o.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z,this.stopPropagation()}},o.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){o.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!o.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.focusinBubbles||o.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){o.event.simulate(b,a.target,o.event.fix(a),!0)};o.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),o.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return o().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=o.guid++)),this.each(function(){o.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,o(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){o.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){o.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?o.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return o.nodeName(a,"table")&&o.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)o.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=o.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&o.nodeName(a,b)?o.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}o.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=o.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||o.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,n=a.length;n>m;m++)if(e=a[m],e||0===e)if("object"===o.type(e))o.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;o.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===o.inArray(e,d))&&(i=o.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f,g,h=o.event.special,i=0;void 0!==(c=a[i]);i++){if(o.acceptData(c)&&(f=c[L.expando],f&&(b=L.cache[f]))){if(d=Object.keys(b.events||{}),d.length)for(g=0;void 0!==(e=d[g]);g++)h[e]?o.event.remove(c,e):o.removeEvent(c,e,b.handle);L.cache[f]&&delete L.cache[f]}delete M.cache[c[M.expando]]}}}),o.fn.extend({text:function(a){return J(this,function(a){return void 0===a?o.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?o.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||o.cleanData(ob(c)),c.parentNode&&(b&&o.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(o.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return o.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(o.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,o.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,n=k-1,p=a[0],q=o.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(c=o.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=o.map(ob(c,"script"),kb),g=f.length;k>j;j++)h=c,j!==n&&(h=o.clone(h,!0,!0),g&&o.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,o.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&o.contains(i,h)&&(h.src?o._evalUrl&&o._evalUrl(h.src):o.globalEval(h.textContent.replace(hb,"")))}return this}}),o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){o.fn[a]=function(a){for(var c,d=[],e=o(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),o(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d=o(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:o.css(d[0],"display");return d.detach(),e}function tb(a){var b=m,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||o("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||o.contains(a.ownerDocument,a)||(g=o.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",e=m.documentElement,f=m.createElement("div"),g=m.createElement("div");g.style.backgroundClip="content-box",g.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===g.style.backgroundClip,f.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",f.appendChild(g);function h(){g.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",e.appendChild(f);var d=a.getComputedStyle(g,null);b="1%"!==d.top,c="4px"===d.width,e.removeChild(f)}a.getComputedStyle&&o.extend(l,{pixelPosition:function(){return h(),b},boxSizingReliable:function(){return null==c&&h(),c},reliableMarginRight:function(){var b,c=g.appendChild(m.createElement("div"));return c.style.cssText=g.style.cssText=d,c.style.marginRight=c.style.width="0",g.style.width="1px",e.appendChild(f),b=!parseFloat(a.getComputedStyle(c,null).marginRight),e.removeChild(f),g.innerHTML="",b}})}(),o.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:0,fontWeight:400},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=o.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=o.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=o.css(a,"border"+R[f]+"Width",!0,e))):(g+=o.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=o.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===o.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):f[g]||(e=S(d),(c&&"none"!==c||!e)&&L.set(d,"olddisplay",e?c:o.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}o.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=o.camelCase(b),i=a.style;return b=o.cssProps[h]||(o.cssProps[h]=Fb(i,h)),g=o.cssHooks[b]||o.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(o.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||o.cssNumber[h]||(c+="px"),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]="",i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=o.camelCase(b);return b=o.cssProps[h]||(o.cssProps[h]=Fb(a.style,h)),g=o.cssHooks[b]||o.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||o.isNumeric(f)?f||0:e):e}}),o.each(["height","width"],function(a,b){o.cssHooks[b]={get:function(a,c,d){return c?0===a.offsetWidth&&zb.test(o.css(a,"display"))?o.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===o.css(a,"boxSizing",!1,e),e):0)}}}),o.cssHooks.marginRight=yb(l.reliableMarginRight,function(a,b){return b?o.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),o.each({margin:"",padding:"",border:"Width"},function(a,b){o.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(o.cssHooks[a+b].set=Gb)}),o.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(o.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=o.css(a,b[g],!1,d);return f}return void 0!==c?o.style(a,b,c):o.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?o(this).show():o(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}o.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(o.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?o.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=o.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){o.fx.step[a.prop]?o.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[o.cssProps[a.prop]]||o.cssHooks[a.prop])?o.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},o.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},o.fx=Kb.prototype.init,o.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(o.cssNumber[a]?"":"px"),g=(o.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(o.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,o.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=o.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k=this,l={},m=a.style,n=a.nodeType&&S(a),p=L.get(a,"fxshow");c.queue||(h=o._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,k.always(function(){k.always(function(){h.unqueued--,o.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],j=o.css(a,"display"),"none"===j&&(j=tb(a.nodeName)),"inline"===j&&"none"===o.css(a,"float")&&(m.display="inline-block")),c.overflow&&(m.overflow="hidden",k.always(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(n?"hide":"show")){if("show"!==e||!p||void 0===p[d])continue;n=!0}l[d]=p&&p[d]||o.style(a,d)}if(!o.isEmptyObject(l)){p?"hidden"in p&&(n=p.hidden):p=L.access(a,"fxshow",{}),f&&(p.hidden=!n),n?o(a).show():k.done(function(){o(a).hide()}),k.done(function(){var b;L.remove(a,"fxshow");for(b in l)o.style(a,b,l[b])});for(d in l)g=Ub(n?p[d]:0,d,k),d in p||(p[d]=g.start,n&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=o.camelCase(c),e=b[d],f=a[c],o.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=o.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=o.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:o.extend({},b),opts:o.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=o.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return o.map(k,Ub,j),o.isFunction(j.opts.start)&&j.opts.start.call(a,j),o.fx.timer(o.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}o.Animation=o.extend(Xb,{tweener:function(a,b){o.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),o.speed=function(a,b,c){var d=a&&"object"==typeof a?o.extend({},a):{complete:c||!c&&b||o.isFunction(a)&&a,duration:a,easing:c&&b||b&&!o.isFunction(b)&&b};return d.duration=o.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in o.fx.speeds?o.fx.speeds[d.duration]:o.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){o.isFunction(d.old)&&d.old.call(this),d.queue&&o.dequeue(this,d.queue)},d},o.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=o.isEmptyObject(a),f=o.speed(b,c,d),g=function(){var b=Xb(this,o.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=o.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&o.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=o.timers,g=d?d.length:0;for(c.finish=!0,o.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),o.each(["toggle","show","hide"],function(a,b){var c=o.fn[b];o.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),o.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){o.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),o.timers=[],o.fx.tick=function(){var a,b=0,c=o.timers;for(Lb=o.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||o.fx.stop(),Lb=void 0},o.fx.timer=function(a){o.timers.push(a),a()?o.fx.start():o.timers.pop()},o.fx.interval=13,o.fx.start=function(){Mb||(Mb=setInterval(o.fx.tick,o.fx.interval))},o.fx.stop=function(){clearInterval(Mb),Mb=null},o.fx.speeds={slow:600,fast:200,_default:400},o.fn.delay=function(a,b){return a=o.fx?o.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=m.createElement("input"),b=m.createElement("select"),c=b.appendChild(m.createElement("option"));a.type="checkbox",l.checkOn=""!==a.value,l.optSelected=c.selected,b.disabled=!0,l.optDisabled=!c.disabled,a=m.createElement("input"),a.value="t",a.type="radio",l.radioValue="t"===a.value}();var Yb,Zb,$b=o.expr.attrHandle;o.fn.extend({attr:function(a,b){return J(this,o.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){o.removeAttr(this,a)})}}),o.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?o.prop(a,b,c):(1===f&&o.isXMLDoc(a)||(b=b.toLowerCase(),d=o.attrHooks[b]||(o.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=o.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void o.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=o.propFix[c]||c,o.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&o.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?o.removeAttr(a,c):a.setAttribute(c,c),c}},o.each(o.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||o.find.attr;$b[b]=function(a,b,d){var e,f;
+return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;o.fn.extend({prop:function(a,b){return J(this,o.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[o.propFix[a]||a]})}}),o.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!o.isXMLDoc(a),f&&(b=o.propFix[b]||b,e=o.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),l.optSelected||(o.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),o.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){o.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;o.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(o.isFunction(a))return this.each(function(b){o(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=o.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(o.isFunction(a))return this.each(function(b){o(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?o.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(o.isFunction(a)?function(c){o(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=o(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;o.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=o.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,o(this).val()):a,null==e?e="":"number"==typeof e?e+="":o.isArray(e)&&(e=o.map(e,function(a){return null==a?"":a+""})),b=o.valHooks[this.type]||o.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=o.valHooks[e.type]||o.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),o.extend({valHooks:{select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(l.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&o.nodeName(c.parentNode,"optgroup"))){if(b=o(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=o.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=o.inArray(o(d).val(),f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),o.each(["radio","checkbox"],function(){o.valHooks[this]={set:function(a,b){return o.isArray(b)?a.checked=o.inArray(o(a).val(),b)>=0:void 0}},l.checkOn||(o.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),o.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){o.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),o.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=o.now(),dc=/\?/;o.parseJSON=function(a){return JSON.parse(a+"")},o.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&o.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=m.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(o.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,o.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=o.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&o.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}o.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":o.parseJSON,"text xml":o.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,o.ajaxSettings),b):tc(o.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=o.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?o(l):o.event,n=o.Deferred(),p=o.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(n.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=o.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=o.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===o.active++&&o.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(o.lastModified[d]&&v.setRequestHeader("If-Modified-Since",o.lastModified[d]),o.etag[d]&&v.setRequestHeader("If-None-Match",o.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(o.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(o.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?n.resolveWith(l,[r,x,v]):n.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--o.active||o.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return o.get(a,b,c,"json")},getScript:function(a,b){return o.get(a,void 0,b,"script")}}),o.each(["get","post"],function(a,b){o[b]=function(a,c,d,e){return o.isFunction(c)&&(e=e||d,d=c,c=void 0),o.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),o.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){o.fn[b]=function(a){return this.on(b,a)}}),o._evalUrl=function(a){return o.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},o.fn.extend({wrapAll:function(a){var b;return o.isFunction(a)?this.each(function(b){o(this).wrapAll(a.call(this,b))}):(this[0]&&(b=o(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(o.isFunction(a)?function(b){o(this).wrapInner(a.call(this,b))}:function(){var b=o(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=o.isFunction(a);return this.each(function(c){o(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){o.nodeName(this,"body")||o(this).replaceWith(this.childNodes)}).end()}}),o.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},o.expr.filters.visible=function(a){return!o.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(o.isArray(b))o.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==o.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}o.param=function(a,b){var c,d=[],e=function(a,b){b=o.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=o.ajaxSettings&&o.ajaxSettings.traditional),o.isArray(a)||a.jquery&&!o.isPlainObject(a))o.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},o.fn.extend({serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=o.prop(this,"elements");return a?o.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!o(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=o(this).val();return null==c?null:o.isArray(c)?o.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),o.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=o.ajaxSettings.xhr();a.ActiveXObject&&o(a).on("unload",function(){for(var a in Dc)Dc[a]()}),l.cors=!!Fc&&"withCredentials"in Fc,l.ajax=Fc=!!Fc,o.ajaxTransport(function(a){var b;return l.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort"),f.send(a.hasContent&&a.data||null)},abort:function(){b&&b()}}:void 0}),o.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return o.globalEval(a),a}}}),o.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),o.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=o("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),m.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Gc=[],Hc=/(=)\?(?=&|$)|\?\?/;o.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Gc.pop()||o.expando+"_"+cc++;return this[a]=!0,a}}),o.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Hc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Hc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=o.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Hc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||o.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Gc.push(e)),g&&o.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),o.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||m;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=o.buildFragment([a],b,e),e&&e.length&&o(e).remove(),o.merge([],d.childNodes))};var Ic=o.fn.load;o.fn.load=function(a,b,c){if("string"!=typeof a&&Ic)return Ic.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=a.slice(h),a=a.slice(0,h)),o.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&o.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?o("<div>").append(o.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},o.expr.filters.animated=function(a){return o.grep(o.timers,function(b){return a===b.elem}).length};var Jc=a.document.documentElement;function Kc(a){return o.isWindow(a)?a:9===a.nodeType&&a.defaultView}o.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=o.css(a,"position"),l=o(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=o.css(a,"top"),i=o.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),o.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},o.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){o.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,o.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Kc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===o.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),o.nodeName(a[0],"html")||(d=a.offset()),d.top+=o.css(a[0],"borderTopWidth",!0),d.left+=o.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-o.css(c,"marginTop",!0),left:b.left-d.left-o.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Jc;while(a&&!o.nodeName(a,"html")&&"static"===o.css(a,"position"))a=a.offsetParent;return a||Jc})}}),o.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;o.fn[b]=function(e){return J(this,function(b,e,f){var g=Kc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),o.each(["top","left"],function(a,b){o.cssHooks[b]=yb(l.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?o(a).position()[b]+"px":c):void 0})}),o.each({Height:"height",Width:"width"},function(a,b){o.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){o.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return o.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?o.css(b,c,g):o.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),o.fn.size=function(){return this.length},o.fn.andSelf=o.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return o});var Lc=a.jQuery,Mc=a.$;return o.noConflict=function(b){return a.$===o&&(a.$=Mc),b&&a.jQuery===o&&(a.jQuery=Lc),o},typeof b===U&&(a.jQuery=a.$=o),o});
diff --git a/lib/jquery.event.ue.js b/lib/jquery.event.ue.js
new file mode 100644 (file)
index 0000000..efda624
--- /dev/null
@@ -0,0 +1,769 @@
+/*
+ * Jquery plugin for unified mouse and touch events
+ *
+ * Copyright (c) 2013 Michael S. Mikowski
+ * (mike[dot]mikowski[at]gmail[dotcom])
+ *
+ * Dual licensed under the MIT or GPL Version 2
+ * http://jquery.org/license
+ *
+ * Versions
+ *  0.3.0 - Initial jQuery plugin site release
+ *        - Replaced scrollwheel zoom with drag motion.
+ *          This resolved a conflict with scrollable areas.
+ *  0.3.1 - Change for jQuery plugins site
+ *  0.3.2 - Updated to jQuery 1.9.1.
+ *          Confirmed 1.7.0-1.9.1 compatibility.
+ *  0.4.2 - Updated documentation
+ *  0.4.3 - Removed fatal execption possibility if originalEvent
+ *          is not defined on event object
+ *
+*/
+
+/*jslint           browser : true,   continue : true,
+  devel  : true,    indent : 2,       maxerr  : 50,
+  newcap : true,  plusplus : true,    regexp  : true,
+  sloppy : true,      vars : true,     white  : true
+*/
+/*global jQuery, sl */
+
+(function ( $ ) {
+  //---------------- BEGIN MODULE SCOPE VARIABLES --------------
+  var
+    $Special        = $.event.special,  // shortcut for special event
+    motionMapMap    = {},         // map of pointer motions by cursor
+    isMoveBound     = false,      // flag if move handlers bound
+    pxPinchZoom     = -1,         // distance between pinch-zoom points
+    optionKey       = 'ue_bound', // data key for storing options
+    doDisableMouse  = false,      // flag to discard mouse input
+    defaultOptMap   = {           // Default option hash
+      bound_ns_map  : {},         // namspace hash e.g. bound_ns_map.utap.fred
+      wheel_ratio   : 15,         // multiplier for mousewheel delta
+      px_radius     : 3,          // 'distance' dragged before dragstart
+      ignore_class  : ':input',   // 'not' suppress matching elements
+      tap_time      : 200,        // millisecond max time to consider tap
+      held_tap_time : 300         // millisecond min time to consider taphold
+    },
+    callbackList  = [],           // global callback stack
+    zoomMouseNum  = 1,            // multiplier for mouse zoom
+    zoomTouchNum  = 4,            // multiplier for touch zoom
+
+    boundList, Ue,
+    motionDragId,  motionHeldId, motionDzoomId,
+    motion1ZoomId, motion2ZoomId,
+
+    checkMatchVal, removeListVal,  pushUniqVal,   makeListPlus,
+    fnHeld,        fnMotionStart,  fnMotionMove,
+    fnMotionEnd,   onMouse,        onTouch,
+    onMousewheel
+    ;
+  //----------------- END MODULE SCOPE VARIABLES ---------------
+
+  //------------------- BEGIN UTILITY METHODS ------------------
+  // Begin utiltity /makeListPlus/
+  // Returns an array with much desired methods:
+  //   * remove_val(value) : remove element that matches
+  //     the provided value. Returns number of elements
+  //     removed.
+  //   * match_val(value)  : shows if a value exists
+  //   * push_uniq(value)  : pushes a value onto the stack
+  //     iff it does not already exist there
+  // Note: the reason I need this is to compare objects to
+  //   objects (perhaps jQuery has something similar?)
+  checkMatchVal = function ( data ) {
+    var match_count = 0, idx;
+    for ( idx = this.length; idx; 0 ) {
+      if ( this[--idx] === data ) { match_count++; }
+    }
+    return match_count;
+  };
+  removeListVal = function ( data ) {
+    var removed_count = 0, idx;
+    for ( idx = this.length; idx; 0 ) {
+      if ( this[--idx] === data ) {
+        this.splice(idx, 1);
+        removed_count++;
+        idx++;
+      }
+    }
+    return removed_count;
+  };
+  pushUniqVal = function ( data ) {
+    if ( checkMatchVal.call(this, data ) ) { return false; }
+    this.push( data );
+    return true;
+  };
+  // primary utility
+  makeListPlus = function ( input_list ) {
+    if ( input_list && $.isArray(input_list) ) {
+      if ( input_list.remove_val ) {
+        console.warn( 'The array appears to already have listPlus capabilities' );
+        return input_list;
+      }
+    }
+    else {
+      input_list = [];
+    }
+    input_list.remove_val = removeListVal;
+    input_list.match_val  = checkMatchVal;
+    input_list.push_uniq  = pushUniqVal;
+
+    return input_list;
+  };
+  // End utility /makeListPlus/
+  //-------------------- END UTILITY METHODS -------------------
+
+  //--------------- BEGIN JQUERY SPECIAL EVENTS ----------------
+  // Unique array for bound objects
+  boundList = makeListPlus();
+
+  // Begin define special event handlers
+  Ue = {
+    setup : function( data, a_names, fn_bind ) {
+      var
+        this_el     = this,
+        $to_bind    = $(this_el),
+        seen_map    = {},
+        option_map, idx, namespace_key, ue_namespace_code, namespace_list
+        ;
+
+      // if previous related event bound do not rebind, but do add to
+      // type of event bound to this element, if not already noted
+      if ( $.data( this, optionKey ) ) { return; }
+
+      option_map = {};
+      $.extend( true, option_map, defaultOptMap );
+      $.data( this_el, optionKey, option_map );
+
+      namespace_list = makeListPlus(a_names.slice(0));
+      if ( ! namespace_list.length 
+        || namespace_list[0] === ""
+      ) { namespace_list = ["000"]; }
+
+      NSPACE_00:
+      for ( idx = 0; idx < namespace_list.length; idx++ ) {
+        namespace_key = namespace_list[idx];
+
+        if ( ! namespace_key ) { continue NSPACE_00; }
+        if ( seen_map.hasOwnProperty(namespace_key) ) { continue NSPACE_00; }
+
+        seen_map[namespace_key] = true;
+
+        ue_namespace_code = '.__ue' + namespace_key;
+
+        $to_bind.bind( 'mousedown'  + ue_namespace_code, onMouse  );
+        $to_bind.bind( 'touchstart' + ue_namespace_code, onTouch );
+        $to_bind.bind( 'mousewheel' + ue_namespace_code, onMousewheel );
+      }
+
+      boundList.push_uniq( this_el ); // record as bound element
+
+      if ( ! isMoveBound ) {
+        // console.log('first element bound - adding global binds');
+        $(document).bind( 'mousemove.__ue',   onMouse );
+        $(document).bind( 'touchmove.__ue',   onTouch );
+        $(document).bind( 'mouseup.__ue'  ,   onMouse );
+        $(document).bind( 'touchend.__ue' ,   onTouch );
+        $(document).bind( 'touchcancel.__ue', onTouch );
+        isMoveBound = true;
+      }
+    },
+
+    // arg_map.type = string - name of event to bind
+    // arg_map.data = poly - whatever (optional) data was passed when binding
+    // arg_map.namespace = string - A sorted, dot-delimited list of namespaces
+    //   specified when binding the event
+    // arg_map.handler  = fn - the event handler the developer wishes to be bound
+    //   to the event.  This function should be called whenever the event
+    //   is triggered
+    // arg_map.guid = number - unique ID for event handler, provided by jQuery
+    // arg_map.selector = string - selector used by 'delegate' or 'live' jQuery
+    //   methods.  Only available when these methods are used.
+    //
+    // this - the element to which the event handler is being bound
+    // this always executes immediate after setup (if first binding)
+    add : function ( arg_map ) {
+      var
+        this_el         = this,
+        option_map      = $.data( this_el, optionKey ),
+        namespace_str   = arg_map.namespace,
+        event_type      = arg_map.type,
+        bound_ns_map, namespace_list, idx, namespace_key
+        ;
+      if ( ! option_map ) { return; }
+
+      bound_ns_map  = option_map.bound_ns_map;
+
+      if ( ! bound_ns_map[event_type] ) {
+        // this indicates a non-namespaced entry
+        bound_ns_map[event_type] = {};
+      }
+
+      if ( ! namespace_str ) { return; }
+
+      namespace_list = namespace_str.split('.');
+
+      for ( idx = 0; idx < namespace_list.length; idx++ ) {
+        namespace_key = namespace_list[idx];
+        bound_ns_map[event_type][namespace_key] = true;
+      }
+    },
+
+    remove : function ( arg_map ) {
+      var
+        elem_bound     = this,
+        option_map     = $.data( elem_bound, optionKey ),
+        bound_ns_map   = option_map.bound_ns_map,
+        event_type     = arg_map.type,
+        namespace_str  = arg_map.namespace,
+        namespace_list, idx, namespace_key
+        ;
+
+      if ( ! bound_ns_map[event_type] ) { return; }
+
+      // No namespace(s) provided:
+      // Remove complete record for custom event type (e.g. utap)
+      if ( ! namespace_str ) {
+        delete bound_ns_map[event_type];
+        return;
+      }
+
+      // Namespace(s) provided:
+      // Remove namespace flags from each custom event typei (e.g. utap)
+      // record.  If all claimed namespaces are removed, remove
+      // complete record.
+      namespace_list = namespace_str.split('.');
+
+      for ( idx = 0; idx < namespace_list.length; idx++ ) {
+        namespace_key = namespace_list[idx];
+        if (bound_ns_map[event_type][namespace_key]) {
+          delete bound_ns_map[event_type][namespace_key];
+        }
+      }
+
+      if ( $.isEmptyObject( bound_ns_map[event_type] ) ) {
+        delete bound_ns_map[event_type];
+      }
+    },
+
+    teardown : function( a_names ) {
+      var
+        elem_bound   = this,
+        $bound       = $(elem_bound),
+        option_map   = $.data( elem_bound, optionKey ),
+        bound_ns_map = option_map.bound_ns_map,
+        idx, namespace_key, ue_namespace_code, namespace_list
+        ;
+
+      // do not tear down if related handlers are still bound
+      if ( ! $.isEmptyObject( bound_ns_map ) ) { return; }
+
+      namespace_list = makeListPlus(a_names);
+      namespace_list.push_uniq('000');
+
+      NSPACE_01:
+      for ( idx = 0; idx < namespace_list.length; idx++ ) {
+        namespace_key = namespace_list[idx];
+
+        if ( ! namespace_key ) { continue NSPACE_01; }
+
+        ue_namespace_code = '.__ue' + namespace_key;
+        $bound.unbind( 'mousedown'  + ue_namespace_code );
+        $bound.unbind( 'touchstart' + ue_namespace_code );
+        $bound.unbind( 'mousewheel' + ue_namespace_code );
+      }
+
+      $.removeData( elem_bound, optionKey );
+
+      // Unbind document events only after last element element is removed
+      boundList.remove_val(this);
+      if ( boundList.length === 0 ) {
+        // console.log('last bound element removed - removing global binds');
+        $(document).unbind( 'mousemove.__ue');
+        $(document).unbind( 'touchmove.__ue');
+        $(document).unbind( 'mouseup.__ue');
+        $(document).unbind( 'touchend.__ue');
+        $(document).unbind( 'touchcancel.__ue');
+        isMoveBound = false;
+      }
+    }
+  };
+  // End define special event handlers
+  //--------------- BEGIN JQUERY SPECIAL EVENTS ----------------
+
+  //------------------ BEGIN MOTION CONTROLS -------------------
+  // Begin motion control /fnHeld/
+  fnHeld = function ( arg_map ) {
+    var
+      timestamp         = +new Date(),
+      motion_id    = arg_map.motion_id,
+      motion_map   = arg_map.motion_map,
+      bound_ns_map = arg_map.bound_ns_map,
+      event_ue
+      ;
+
+    delete motion_map.idto_tapheld;
+
+    if ( ! motion_map.do_allow_tap ) { return; }
+
+    motion_map.px_end_x     = motion_map.px_start_x;
+    motion_map.px_end_y     = motion_map.px_start_y;
+    motion_map.ms_timestop  = timestamp;
+    motion_map.ms_elapsed   = timestamp - motion_map.ms_timestart;
+
+    if ( bound_ns_map.uheld ) {
+      event_ue     = $.Event('uheld');
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+    }
+
+    // remove tracking, as we want no futher action on this motion
+    if ( bound_ns_map.uheldstart ) {
+      event_ue     = $.Event('uheldstart');
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+      motionHeldId = motion_id;
+    }
+    else {
+      delete motionMapMap[motion_id];
+    }
+  };
+  // End motion control /fnHeld/
+
+
+  // Begin motion control /fnMotionStart/
+  fnMotionStart = function ( arg_map ) {
+    var
+      motion_id      = arg_map.motion_id,
+      event_src      = arg_map.event_src,
+      request_dzoom  = arg_map.request_dzoom,
+
+      option_map     = $.data( arg_map.elem, optionKey ),
+      bound_ns_map   = option_map.bound_ns_map,
+      $target        = $(event_src.target ),
+      do_zoomstart   = false,
+      motion_map, cb_map, do_allow_tap, event_ue
+      ;
+
+    // this should never happen, but it does
+    if ( motionMapMap[ motion_id ] ) { return; }
+
+    if ( request_dzoom && ! bound_ns_map.uzoomstart ) { return; }
+
+    // :input selector includes text areas
+    if ( $target.is( option_map.ignore_class ) ) { return; }
+
+    do_allow_tap = bound_ns_map.utap
+      || bound_ns_map.uheld || bound_ns_map.uheldstart
+      ? true : false;
+
+    cb_map = callbackList.pop();
+
+    while ( cb_map ) {
+      if ( $target.is( cb_map.selector_str )
+        || $( arg_map.elem ).is( cb_map.selector_str )
+      ) {
+        if ( cb_map.callback_match ) {
+          cb_map.callback_match( arg_map );
+        }
+      }
+      else {
+        if ( cb_map.callback_nomatch ) {
+          cb_map.callback_nomatch( arg_map );
+        }
+      }
+      cb_map = callbackList.pop();
+    }
+
+    motion_map = {
+      do_allow_tap : do_allow_tap,
+      elem_bound   : arg_map.elem,
+      elem_target  : event_src.target,
+      ms_elapsed   : 0,
+      ms_timestart : event_src.timeStamp,
+      ms_timestop  : undefined,
+      option_map   : option_map,
+      orig_target  : event_src.target,
+      px_current_x : event_src.clientX,
+      px_current_y : event_src.clientY,
+      px_end_x     : undefined,
+      px_end_y     : undefined,
+      px_start_x   : event_src.clientX,
+      px_start_y   : event_src.clientY,
+      timeStamp    : event_src.timeStamp
+    };
+
+    motionMapMap[ motion_id ] = motion_map;
+
+    if ( bound_ns_map.uzoomstart ) {
+      if ( request_dzoom ) {
+        motionDzoomId = motion_id;
+      }
+      else if ( ! motion1ZoomId ) {
+        motion1ZoomId = motion_id;
+      }
+      else if ( ! motion2ZoomId ) {
+        motion2ZoomId = motion_id;
+        event_ue = $.Event('uzoomstart');
+        do_zoomstart = true;
+      }
+
+      if ( do_zoomstart ) {
+        event_ue = $.Event( 'uzoomstart' );
+        motion_map.px_delta_zoom = 0;
+        $.extend( event_ue, motion_map );
+        $(motion_map.elem_bound).trigger(event_ue);
+        return;
+      }
+    }
+
+    if ( bound_ns_map.uheld || bound_ns_map.uheldstart ) {
+      motion_map.idto_tapheld = setTimeout(
+        function() {
+          fnHeld({
+            motion_id  : motion_id,
+            motion_map   : motion_map,
+            bound_ns_map : bound_ns_map
+          });
+        },
+        option_map.held_tap_time
+      );
+    }
+  };
+  // End motion control /fnMotionStart/
+
+  // Begin motion control /fnMotionMove/
+  fnMotionMove  = function ( arg_map ) {
+    var
+      motion_id   = arg_map.motion_id,
+      event_src   = arg_map.event_src,
+      do_zoommove = false,
+      motion_map, option_map, bound_ns_map,
+      event_ue, px_pinch_zoom, px_delta_zoom,
+      mzoom1_map, mzoom2_map
+      ;
+
+    if ( ! motionMapMap[motion_id] ) { return; }
+
+    motion_map   = motionMapMap[motion_id];
+    option_map   = motion_map.option_map;
+    bound_ns_map = option_map.bound_ns_map;
+
+    motion_map.timeStamp    = event_src.timeStamp;
+    motion_map.elem_target  = event_src.target;
+    motion_map.ms_elapsed   = event_src.timeStamp - motion_map.ms_timestart;
+
+    motion_map.px_delta_x   = event_src.clientX - motion_map.px_current_x;
+    motion_map.px_delta_y   = event_src.clientY - motion_map.px_current_y;
+
+    motion_map.px_current_x = event_src.clientX;
+    motion_map.px_current_y = event_src.clientY;
+
+    // native event object override
+    motion_map.timeStamp    = event_src.timeStamp;
+
+    // disallow tap if outside of zone or time elapsed
+    // we use this for other events, so we do it every time
+    if ( motion_map.do_allow_tap ) {
+      if ( Math.abs(motion_map.px_delta_x) > option_map.px_radius
+        || Math.abs(motion_map.pd_delta_y) > option_map.px_radius
+        || motion_map.ms_elapsed           > option_map.tap_time
+      ) { motion_map.do_allow_tap = false; }
+    }
+
+    if ( motion1ZoomId && motion2ZoomId
+      && ( motion_id === motion1ZoomId
+        || motion_id === motion2ZoomId
+    )) {
+      motionMapMap[motion_id] = motion_map;
+      mzoom1_map = motionMapMap[motion1ZoomId];
+      mzoom2_map = motionMapMap[motion2ZoomId];
+
+      px_pinch_zoom = Math.floor(
+        Math.sqrt(
+            Math.pow((mzoom1_map.px_current_x - mzoom2_map.px_current_x),2)
+          + Math.pow((mzoom1_map.px_current_y - mzoom2_map.px_current_y),2)
+        ) +0.5
+      );
+
+      if ( pxPinchZoom === -1 ) { px_delta_zoom = 0; }
+      else { px_delta_zoom = ( px_pinch_zoom - pxPinchZoom ) * zoomTouchNum;}
+
+      // save value for next iteration delta comparison
+      pxPinchZoom  = px_pinch_zoom;
+      do_zoommove  = true;
+    }
+    else if ( motionDzoomId === motion_id ) {
+      if ( bound_ns_map.uzoommove ) {
+        px_delta_zoom = motion_map.px_delta_y * zoomMouseNum;
+        do_zoommove = true;
+      }
+    }
+
+    if ( do_zoommove ){
+      event_ue = $.Event('uzoommove');
+      motion_map.px_delta_zoom = px_delta_zoom;
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+      return;
+    }
+
+    if ( motionHeldId === motion_id ) {
+      if ( bound_ns_map.uheldmove ) {
+        event_ue = $.Event('uheldmove');
+        $.extend( event_ue, motion_map );
+        $(motion_map.elem_bound).trigger(event_ue);
+        event_src.preventDefault();
+      }
+    }
+    else if ( motionDragId === motion_id ) {
+      if ( bound_ns_map.udragmove ) {
+        event_ue = $.Event('udragmove');
+        $.extend( event_ue, motion_map );
+        $(motion_map.elem_bound).trigger(event_ue);
+        event_src.preventDefault();
+      }
+    }
+
+    if ( ! motionDragId
+      && ! motionHeldId
+      && bound_ns_map.udragstart
+      && motion_map.do_allow_tap === false
+    ) {
+      motionDragId = motion_id;
+      event_ue = $.Event('udragstart');
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+      event_src.preventDefault();
+
+      if ( motion_map.idto_tapheld ) {
+        clearTimeout(motion_map.idto_tapheld);
+        delete motion_map.idto_tapheld;
+      }
+    }
+  };
+  // End motion control /fnMotionMove/
+
+  // Begin motion control /fnMotionEnd/
+  fnMotionEnd   = function ( arg_map ) {
+    var
+      motion_id    = arg_map.motion_id,
+      event_src    = arg_map.event_src,
+      do_zoomend   = false,
+      motion_map, option_map, bound_ns_map, event_ue
+      ;
+
+    doDisableMouse = false;
+
+    if ( ! motionMapMap[motion_id] ) { return; }
+
+    motion_map   = motionMapMap[motion_id];
+    option_map   = motion_map.option_map;
+    bound_ns_map = option_map.bound_ns_map;
+
+    motion_map.elem_target  = event_src.target;
+    motion_map.ms_elapsed   = event_src.timeStamp - motion_map.ms_timestart;
+    motion_map.ms_timestop  = event_src.timeStamp;
+
+    if ( motion_map.px_current_x ) {
+      motion_map.px_delta_x   = event_src.clientX - motion_map.px_current_x;
+      motion_map.px_delta_y   = event_src.clientY - motion_map.px_current_y;
+    }
+
+    motion_map.px_current_x = event_src.clientX;
+    motion_map.px_current_y = event_src.clientY;
+
+    motion_map.px_end_x     = event_src.clientX;
+    motion_map.px_end_y     = event_src.clientY;
+
+    // native event object override
+    motion_map.timeStamp    = event_src.timeStamp
+    ;
+
+    // clear-out any long-hold tap timer
+    if ( motion_map.idto_tapheld ) {
+      clearTimeout(motion_map.idto_tapheld);
+      delete motion_map.idto_tapheld;
+    }
+
+    // trigger utap
+    if ( bound_ns_map.utap
+      && motion_map.ms_elapsed   <= option_map.tap_time
+      && motion_map.do_allow_tap
+    ) {
+      event_ue = $.Event('utap');
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+    }
+
+    // trigger udragend
+    if ( motion_id === motionDragId ) {
+      if ( bound_ns_map.udragend ) {
+        event_ue = $.Event('udragend');
+        $.extend( event_ue, motion_map );
+        $(motion_map.elem_bound).trigger(event_ue);
+        event_src.preventDefault();
+      }
+      motionDragId = undefined;
+    }
+
+    // trigger heldend
+    if ( motion_id === motionHeldId ) {
+      if ( bound_ns_map.uheldend ) {
+        event_ue = $.Event('uheldend');
+        $.extend( event_ue, motion_map );
+        $(motion_map.elem_bound).trigger(event_ue);
+      }
+      motionHeldId = undefined;
+    }
+
+    // trigger uzoomend
+    if ( motion_id === motionDzoomId ) {
+      do_zoomend = true;
+      motionDzoomId = undefined;
+    }
+
+    // cleanup zoom info
+    else if ( motion_id === motion1ZoomId ) {
+      if ( motion2ZoomId ) {
+        motion1ZoomId = motion2ZoomId;
+        motion2ZoomId = undefined;
+        do_zoomend = true;
+      }
+      else { motion1ZoomId = undefined; }
+      pxPinchZoom  = -1;
+    }
+    if ( motion_id === motion2ZoomId ) {
+      motion2ZoomId = undefined;
+      pxPinchZoom  = -1;
+      do_zoomend   = true;
+    }
+
+    if ( do_zoomend && bound_ns_map.uzoomend ) {
+      event_ue = $.Event('uzoomend');
+      motion_map.px_delta_zoom = 0;
+      $.extend( event_ue, motion_map );
+      $(motion_map.elem_bound).trigger(event_ue);
+    }
+    // remove pointer from consideration
+    delete motionMapMap[motion_id];
+  };
+  // End motion control /fnMotionEnd/
+  //------------------ END MOTION CONTROLS -------------------
+
+ //------------------- BEGIN EVENT HANDLERS -------------------
+  // Begin event handler /onTouch/ for all touch events.
+  // We use the 'type' attribute to dispatch to motion control
+  onTouch = function ( event ) {
+    var
+      this_el     = this,
+      timestamp   = +new Date(),
+      o_event     = event.originalEvent,
+      touch_list  = o_event ? o_event.changedTouches || [] : [],
+      touch_count = touch_list.length,
+      idx, touch_event, motion_id, handler_fn
+      ;
+
+    doDisableMouse = true;
+
+    event.timeStamp = timestamp;
+
+    switch ( event.type ) {
+      case 'touchstart' :
+        handler_fn = fnMotionStart;
+        event.preventDefault();
+      break;
+      case 'touchmove'  :
+        handler_fn = fnMotionMove;
+      break;
+      case 'touchend'    : 
+      case 'touchcancel' : handler_fn = fnMotionEnd;   break;
+      default : handler_fn = null;
+    }
+
+    if ( ! handler_fn ) { return; }
+
+    for ( idx = 0; idx < touch_count; idx++ ) {
+      touch_event  = touch_list[idx];
+
+      motion_id = 'touch' + String(touch_event.identifier);
+
+      event.clientX   = touch_event.clientX;
+      event.clientY   = touch_event.clientY;
+      handler_fn({
+        elem      : this_el,
+        motion_id : motion_id,
+        event_src : event
+      });
+    }
+  };
+  // End event handler /onTouch/
+
+
+  // Begin event handler /onMouse/ for all mouse events
+  // We use the 'type' attribute to dispatch to motion control
+  onMouse = function ( event ) {
+    var
+      this_el       = this,
+      motion_id     = 'mouse' + String(event.button),
+      request_dzoom = false,
+      handler_fn
+      ;
+
+    if ( doDisableMouse ) {
+      event.stopImmediatePropagation();
+      return;
+    }
+
+    if ( event.shiftKey ) { request_dzoom  =  true; }
+
+    // skip left or middle clicks
+    if ( event.type !== 'mousemove' ) {
+      if ( event.button !== 0 ) { return true; }
+    }
+
+    switch ( event.type ) {
+      case 'mousedown' :
+        handler_fn = fnMotionStart;
+        event.preventDefault();
+        break;
+      case 'mouseup'   :
+        handler_fn = fnMotionEnd;
+        break;
+      case 'mousemove' :
+        handler_fn = fnMotionMove;
+        break;
+      default:
+        handler_fn = null;
+    }
+
+    if ( ! handler_fn ) { return; }
+
+    handler_fn({
+      elem          : this_el,
+      event_src     : event,
+      request_dzoom : request_dzoom,
+      motion_id     : motion_id
+    });
+  };
+  // End event handler /onMouse/
+  //-------------------- END EVENT HANDLERS --------------------
+
+
+  // Export special events through jQuery API
+  $Special.ue
+    = $Special.utap       = $Special.uheld
+    = $Special.uzoomstart = $Special.uzoommove = $Special.uzoomend
+    = $Special.udragstart = $Special.udragmove = $Special.udragend
+    = $Special.uheldstart = $Special.uheldmove = $Special.uheldend
+    = Ue
+    ;
+  $.ueSetGlobalCb = function ( selector_str, callback_match, callback_nomatch ) {
+    callbackList.push( {
+      selector_str     : selector_str     || '',
+      callback_match   : callback_match   || null,
+      callback_nomatch : callback_nomatch || null
+    });
+  };
+
+}(jQuery));
diff --git a/lib/jquery.udraggable.js b/lib/jquery.udraggable.js
new file mode 100644 (file)
index 0000000..ef304af
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * jQuery udraggable plugin v0.3.0
+ * Copyright (c) 2013-2014 Grant McLean (grant@mclean.net.nz)
+ *
+ * Homepage: https://github.com/grantm/jquery-udraggable
+ *
+ * Dual licensed under the MIT and GPL (v2.0 or later) licenses:
+ *   http://opensource.org/licenses/MIT
+ *   http://opensource.org/licenses/GPL-2.0
+ *
+ * This library requires Michael S. Mikowski's unified mouse and touch
+ * event plugin: https://github.com/mmikowski/jquery.event.ue
+ *
+ */
+
+(function($) {
+    "use strict";
+
+    var floor = Math.floor;
+    var min   = Math.min;
+    var max   = Math.max;
+
+    window.requestAnimationFrame = window.requestAnimationFrame || function(work) {
+        return setTimeout(work, 10);
+    };
+
+    window.cancelAnimationFrame = window.cancelAnimationFrame || function(id) {
+        return clearTimeout(id);
+    };
+
+
+    // Constructor function
+
+    var UDraggable = function (el, options) {
+        var that = this;
+        this.el  = el;
+        this.$el = $(el);
+        this.options = $.extend({}, $.fn.udraggable.defaults, options);
+        this.positionElement  = this.options.positionElement  || this.positionElement;
+        this.getStartPosition = this.options.getStartPosition || this.getStartPosition;
+        this.updatePositionFrameHandler = function() {
+            delete that.queuedUpdate;
+            var pos = that.ui.position;
+            that.positionElement(that.$el, that.started, pos.left, pos.top);
+            if (that.options.dragUpdate) {
+                that.options.dragUpdate.apply(that.el, [that.ui]);
+            }
+        };
+        this.queuePositionUpdate = function() {
+            if (!that.queuedUpdate) {
+                that.queuedUpdate = window.requestAnimationFrame(that.updatePositionFrameHandler);
+            }
+        };
+        this.init();
+    };
+
+    UDraggable.prototype = {
+
+        constructor: UDraggable,
+
+        init: function() {
+            var that = this;
+            this.disabled = false;
+            this.started = false;
+            this.normalisePosition();
+            var $target = this.options.handle ?
+                          this.$el.find( this.options.handle ) :
+                          this.$el;
+            if (this.options.longPress) {
+                $target
+                    .on('uheldstart.udraggable', function(e) { that.start(e); })
+                    .on('uheldmove.udraggable',  function(e) { that.move(e);  })
+                    .on('uheldend.udraggable',   function(e) { that.end(e);   });
+            }
+            else {
+                $target
+                    .on('udragstart.udraggable', function(e) { that.start(e); })
+                    .on('udragmove.udraggable',  function(e) { that.move(e);  })
+                    .on('udragend.udraggable',   function(e) { that.end(e);   });
+            }
+        },
+
+        destroy: function() {
+            var $target = this.options.handle ?
+                          this.$el.find( this.options.handle ) :
+                          this.$el;
+            $target.off('.udraggable');
+            this.$el.removeData('udraggable');
+        },
+
+        disable: function() {
+            this.disabled = true;
+        },
+
+        enable: function() {
+            this.disabled = false;
+        },
+
+        option: function() {
+            var name;
+            if (arguments.length === 0) {
+                return this.options;
+            }
+            if (arguments.length === 2) {
+                this.options[ arguments[0] ] = arguments[1];
+                return;
+            }
+            if (arguments.length === 1) {
+                if (typeof arguments[0] === 'string') {
+                    return this.options[ arguments[0] ];
+                }
+                if (typeof arguments[0] === 'object') {
+                    for(name in arguments[0]) {
+                        if (arguments[0].hasOwnProperty(name)) {
+                            this.options[name] = arguments[0][name];
+                        }
+                    }
+                }
+            }
+            if (this.options.containment) {
+                this._initContainment();
+            }
+        },
+
+        normalisePosition: function() {
+            var pos = this.$el.position();
+            this.$el.css({
+                position: 'absolute',
+                top: pos.top,
+                left: pos.left,
+                right: 'auto',
+                bottom: 'auto'
+            });
+        },
+
+        start: function(e) {
+            if (this.disabled) {
+                return;
+            }
+            var start = this.getStartPosition(this.$el);
+            this._initContainment();
+            this.ui = {
+                helper:           this.$el,
+                offset:           { top: start.y, left: start.x},
+                originalPosition: { top: start.y, left: start.x},
+                position:         { top: start.y, left: start.x},
+            };
+            if (this.options.longPress) {
+                this._start(e);
+            }
+            return this._stopPropagation(e);
+        },
+
+        move: function(e) {
+            if (this.disabled || (!this.started && !this._start(e))) {
+                return;
+            }
+            var delta_x = e.px_current_x - e.px_start_x;
+            var delta_y = e.px_current_y - e.px_start_y;
+            var axis = this.options.axis;
+            if (axis  &&  axis === "x") {
+                delta_y = 0;
+            }
+            if (axis  &&  axis === "y") {
+                delta_x = 0;
+            }
+            var cur = {
+                left: this.ui.originalPosition.left,
+                top:  this.ui.originalPosition.top
+            };
+            if (!axis  ||  (axis === "x")) {
+                cur.left += delta_x;
+            }
+            if (!axis  ||  (axis === "y")) {
+                cur.top += delta_y;
+            }
+            this._applyGrid(cur);
+            this._applyContainment(cur);
+            var pos = this.ui.position;
+            if ((cur.top !== pos.top)  ||  (cur.left !== pos.left)) {
+                this.ui.position.left = cur.left;
+                this.ui.position.top  = cur.top;
+                this.ui.offset.left   = cur.left;
+                this.ui.offset.top    = cur.top;
+                if (this.options.drag) {
+                    this.options.drag.apply(this.el, [e, this.ui]);
+                }
+                this.queuePositionUpdate();
+            }
+            return this._stopPropagation(e);
+        },
+
+        end: function(e) {
+            if (this.started || this._start(e)) {
+                this.$el.removeClass("udraggable-dragging");
+                this.started = false;
+                if (this.queuedUpdate) {
+                    window.cancelAnimationFrame(this.queuedUpdate);
+                }
+                this.updatePositionFrameHandler();
+                if (this.options.stop) {
+                    this.options.stop.apply(this.el, [e, this.ui]);
+                }
+            }
+            return this._stopPropagation(e);
+        },
+
+        // helper methods
+
+        _stopPropagation: function(e) {
+            e.stopPropagation();
+            e.preventDefault();
+            return false;
+        },
+
+        _start: function(e) {
+            if (!this._mouseDistanceMet(e) || !this._mouseDelayMet(e)) {
+                return;
+            }
+            this.started = true;
+            this.queuePositionUpdate();
+            if (this.options.start) {
+                this.options.start.apply(this.el, [e, this.ui]);
+            }
+            this.$el.addClass("udraggable-dragging");
+            return true;
+        },
+
+        _mouseDistanceMet: function(e) {
+            return max(
+                Math.abs(e.px_start_x - e.px_current_x),
+                Math.abs(e.px_start_y - e.px_current_y)
+            ) >= this.options.distance;
+        },
+
+        _mouseDelayMet: function(e) {
+            return e.ms_elapsed > this.options.delay;
+        },
+
+        _initContainment: function() {
+            var o = this.options;
+            var $c, ce;
+
+            if (!o.containment) {
+                this.containment = null;
+                return;
+            }
+
+            if (o.containment.constructor === Array) {
+                this.containment = o.containment;
+                return;
+            }
+
+            if (o.containment === "parent") {
+                o.containment = this.$el.offsetParent();
+            }
+
+            $c = $( o.containment );
+            ce = $c[ 0 ];
+            if (!ce) {
+                return;
+            }
+
+            this.containment = [
+                0,
+                0,
+                $c.innerWidth() - this.$el.outerWidth(),
+                $c.innerHeight() - this.$el.outerHeight(),
+            ];
+        },
+
+        _applyGrid: function(cur) {
+            if (this.options.grid) {
+                var gx = this.options.grid[0];
+                var gy = this.options.grid[1];
+                cur.left = floor( (cur.left + gx / 2) / gx ) * gx;
+                cur.top  = floor( (cur.top  + gy / 2) / gy ) * gy;
+            }
+        },
+
+        _applyContainment: function(cur) {
+            var cont = this.containment;
+            if (cont) {
+                cur.left = min( max(cur.left, cont[0]), cont[2] );
+                cur.top  = min( max(cur.top,  cont[1]), cont[3] );
+            }
+        },
+
+        getStartPosition: function($el) {
+            return {
+                x: parseInt($el.css('left'), 10) || 0,
+                y: parseInt($el.css('top'),  10) || 0
+            };
+        },
+
+        positionElement: function($el, dragging, left, top) {
+            $el.css({ left: left, top: top });
+        }
+
+    };
+
+
+    // jQuery plugin function
+
+    $.fn.udraggable = function(option) {
+        var args = Array.prototype.slice.call(arguments, 1);
+        var results = [];
+        this.each(function () {
+            var $this = $(this);
+            var data = $this.data('udraggable');
+            if (!data) {
+                data = new UDraggable(this, option);
+                $this.data('udraggable', data);
+            }
+            if (typeof option === 'string') {  // option is a method - call it
+                if(typeof data[option] !== 'function') {
+                    throw "jquery.udraggable has no '" + option + "' method";
+                }
+                var result = data[option].apply(data, args);
+                if (result !== undefined) {
+                    results.push( result );
+                }
+            }
+        });
+        return results.length > 0 ? results[0] : this;
+    };
+
+    $.fn.udraggable.defaults = {
+         axis:        null,
+         delay:       0,
+         distance:    0,
+         longPress:   false,
+         // callbacks
+         drag:        null,
+         start:       null,
+         stop:        null
+    };
+
+
+})(jQuery);
+
index 74a3103..65219e5 100644 (file)
@@ -7,6 +7,9 @@ function check_passwd()
   if (empty($_POST['login']) || empty($_POST['passwd']))
     return false;
 
+  if ($_POST['auth_token'] != $_SESSION['token'])
+    return false;
+
   $sql = sprintf("SELECT * FROM sys_user WHERE login = %s AND passwd = %s",
                 $db->quote($_POST['login']), $db->quote(passwd($_POST['login'], $_POST['passwd'])));
 
@@ -25,9 +28,10 @@ function check_passwd()
                             'login' => $row['login'],
                             'name' => $row['name'],
                             'email' => $row['email'],
-                            'group' => $row['gid'],
                             'theme' => $row['theme'],
+                            'page' => $row['page'],
                             'basedir' => substr($_SERVER['SCRIPT_FILENAME'],0,-9));
+    $_SESSION['sys']['baseurl'] = substr($_SESSION['sys']['basedir'], strlen($_SERVER['DOCUMENT_ROOT']));
     return true;
   }
 
@@ -37,6 +41,21 @@ function check_passwd()
 
 function mask_login()
 {
+  $styles = Styles::instance();
+  $javascript = JavaScript::instance();
+
+  $styles->file('lib/rico3/ricoClient/css/rico.css');
+  $styles->file('lib/rico3/ricoClient/css/rico_icon.css');
+  $styles->file('lib/rico3/ricoClient/css/striping_cupertino.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui_hallinta.css');
+
+  $javascript->file('lib/rico3/ricoClient/js/rico2jqu.js');
+  $javascript->file('lib/rico3/minsrc/rico.js');
+  $javascript->file('lib/rico3/minsrc/ricoLocale_en.js');
+  $javascript->file('lib/rico3/minsrc/ricoUI.js');
+  $javascript->file('lib/rico3/ricoClient/js/ricoThemeroller.js');
+
   $LOGIN_IMG = 'images/login.jpg';
   if (defined('LOGIN_IMG')) $LOGIN_IMG = LOGIN_IMG;
 
@@ -46,55 +65,18 @@ function mask_login()
     $HEIGHT = $info[1];
   }
 
-$html = <<<EOC
-<style type="text/css">
-div.login {
-    background-image: url('$LOGIN_IMG');
-    background-repeat: no-repeat;
-    background-position: center center;
-    margin: -15px;
-}
-
-table.login {
-    border: 1px solid #7b7b7b;
-    background: #f7f7f7;
-    position: relative;
-    top: 290px;
-}
-</style>
-
-<div class="login" id="background">
-<div align="center">
-<form action="index.php" method="POST">
-<table id="logintab" class="login" cellpadding="5">
-<tr><th align="left" colspan="2" style="background: #48b4f8;">Anmeldung</th></tr>
-<tr><th align="right">Login</th><td><input type="text" name="login" id="login" size="15"></td></tr>
-<tr><th align="right">Passwort</th><td><input type="password" name="passwd" size="15"></td></tr>
-<tr><td colspan="2" align="center"><input type="submit" value="Anmelden"></td></tr>
-</table>
-</form>
-</div>
-</div>
-
-<script type="text/javascript">
-var img_height = $HEIGHT;
-var inner_height = window.innerHeight - 50;
-var height = img_height <= inner_height ? img_height : inner_height;
-
-var div = document.getElementById("background");
-div.style.height = height + 'px';
-
-var login = document.getElementById("logintab");
-login.style.position = 'absolute';
-login.style.top = ((window.innerHeight / 2) - (130 / 2)) + 'px';
-login.style.left = ((window.innerWidth / 2) - (120 / 2)) + 'px';
-
-var inp = document.getElementById("login");
-inp.focus();
-</script>
-EOC;
-
-  return $html;
+  $_SESSION = array('token' => md5(microtime().$_SERVER['DOCUMENT_ROOT']));
+  $fname = 'data/misc/login.js';
+  if (file_exists($fname))
+    $javascript->onLoad(str_replace(array('$LOGIN_IMG', '$HEIGHT', '$TOKEN'),
+                                   array($LOGIN_IMG, $HEIGHT, $_SESSION['token']),
+                                   file_get_contents($fname)));
+  $fname = 'data/misc/login.html';
+  if (file_exists($fname))
+    return str_replace(array('$LOGIN_IMG', '$HEIGHT', '$TOKEN'),
+                      array($LOGIN_IMG, $HEIGHT, $_SESSION['token']),
+                      file_get_contents($fname));
+  else
+    error_log('data/misc/login.html missing');
 }
 
-?>
index 936f3e0..a83fd35 100644 (file)
@@ -1,68 +1,61 @@
 <?php
 
+require_once('php.php');
+
 function check_edit($name)
 {
-  global $db;
-
-  $sql = sprintf("SELECT sys_mask.id,edit FROM sys_mask "
-                . "JOIN sys_group_mask ON sys_mask.id = sys_group_mask.mask "
-                . "WHERE gid = %d AND fname = %s "
-                . "ORDER BY edit DESC LIMIT 1",
-                $_SESSION['sys']['group'], $db->quote(str_replace('__','|',$name)));
-
-  $sth = $db->query($sql);
-
-  if ($sth === false) return false;
+    $menu = new MenuItem();
 
-  $row = $sth->fetch();
-  if ($row === false) return false;
-
-  if ($row['edit'] == '0')
-    return false;
-
-  return true;
+    return $menu->mayEdit();
 }
 
-function build_form($name, $mask)
+function build_form($mask)
 {
-  global $jscode;
   $ret = array();
+  $form_checks = array();
+  $label_class = '';
+  $hallinta = Hallinta::instance();
 
-  $jscode[] = "var form_check = {};";
-
-  $ret[] = '<div class="form">';
-  $toggle = '<img src="images/icons/minimise.gif" title="Show second table" id="icon_toggle" onclick="second_toggle()" style="right: 13px; display: none"/>';
-  $close = '<img src="lib/rico/images/close.gif" title="Close" onclick="edit_hide()" />';
-  $edit_title = empty($mask['edit_title']) ? 'Datensatz bearbeiten' : $mask['edit_title'];
-  $ret[] = sprintf('<div class="ricoTitle" style="position: relative;" id="mask_edit_title" class="title"><strong>%s</strong>%s%s</div>',
-                  $edit_title, $toggle, $close);
-  $ret[] = '<form id="form_edit">';
+  $ret[] = '<div style="position: relative; overflow: hidden;">';
+  $ret[] = '<form id="form_edit" style="padding: 2px;">';
 
   $ret[] = '<input type="hidden" id="edit_id" name="id" value="">';
-  $ret[] = sprintf('<input type="hidden" id="edit_source" name="source" value="%s">', $name);
 
   foreach ($mask['edit'] as $id => $info) {
     if ($info['type'] == 'hidden')
       $ret[] = sprintf('<input type="hidden" id="edit_%s" name="%s" value="%s">', $id, $id,
-                      empty($info['default'])?'':$info['default']);
+                      !array_key_exists('default', $info)?'':$info['default']);
     elseif ($info['type'] == 'html')
       $ret[] = $info['code'];
     else {
-      $ret[] = sprintf('<label id="label_%s" for="edit_%s">%s</label><br>', $id, $id, $info['name']);
-      $v = array('id="edit_'.$id.'"',
+      $v = array('id="edit_'.$id."\"",
                 'name="'.$id.'"');
+      if (isset($info['visible'])) {
+         if ($info['visible'] == 'desktop' && $hallinta->isMobile()) return;
+         if ($info['visible'] == 'mobile' && !$hallinta->isMobile()) return;
+         if (($info['visible'] == 'mobile-optional' && $hallinta->isMobile()) ||
+             ($info['visible'] == 'desktop-optional' && !$hallinta->isMobile())) {
+             $label_class = ' class="toggle"';
+             $v[] = 'style="display:none;"';
+         }
+      }
       $checks = array();
 
+      $ret[] = sprintf('<label id="label_%s" for="edit_%s"%s>%s</label>', $id, $id, $label_class, $info['name']);
+
       if (isset($info['required']) && $info['required'] === true) {
        $checks[] = 'required';
        $v[] = 'onblur="form_elem_onblur(event)"';
       }
 
+      if ($info['type'] != 'date')
+       $v[] = 'class="maxwidth"';
+
       if ($info['type'] == 'text' || $info['type'] == 'passwd' ||
          $info['type'] == 'decimal' || $info['type'] == 'number') {
-       $v[] = 'size="'.(empty($info['size'])?'10':$info['size']).'"';
+       $v[] = 'size="'.(!array_key_exists('size', $info)?'10':$info['size']).'"';
        $v[] = 'type="'.($info['type']=='passwd'?'password':'text').'"';
-       $v[] = 'value="'.(empty($info['default'])?'':$info['default']).'"';
+       $v[] = 'value="'.(!array_key_exists('default', $info)?'':$info['default']).'"';
        $ret[] = sprintf('<input %s>', implode(' ', $v));
 
        if ($info['type'] == 'decimal') $checks[] = 'decimal';
@@ -70,17 +63,32 @@ function build_form($name, $mask)
       } elseif ($info['type'] == 'date') {
        $v[] = 'size="8"';
        $ret[] = sprintf('<input %s>&nbsp;', implode(' ', $v));
-       $ret[] = sprintf('<img class="calendar" src="images/icons/calendar.gif" onclick="calendar(\'edit_%s\',event)" />',
+       $ret[] = sprintf('<img class="calendar" src="%simages/icons/calendar.gif" onclick="calendar(\'edit_%s\',event)" />',
+                        Hallinta::instance()->urlbase(),
                         $id, $id, $id);
 
        $checks[] = 'date';
-      } elseif ($info['type'] == 'boolean' || $info['type'] == 'file') {
-       $v[] = 'type="'.($info['type']=='boolean'?'checkbox':'file').'"';
+      } elseif ($info['type'] == 'time') {
+       $v[] = 'size="5"';
+       $ret[] = sprintf('<input %s>&nbsp;', implode(' ', $v));
+       $checks[] = 'time';
+      } elseif ($info['type'] == 'boolean') {
+       $v[] = 'type="checkbox"';
+       $ret[] = sprintf('<input %s>', implode(' ', $v));
+      } elseif ($info['type'] == 'file') {
+       $v[] = 'type="file"';
+       $v[] = 'style="display:none;"';
+       $ret[] = sprintf('<input %s>', implode(' ', $v));
+
+        $v = array('id="edit_real_'.$id.'"',
+                  'name="real_'.$id.'"');
+       $v[] = 'type="text"';
+       $v[] = 'data-for="edit_'.$id.'"';
+       $v[] = 'class="maxwidth file-upload"';
+       $v[] = 'readonly="readonly"';
+       $v[] = 'placeholder="Browse..."';
+       $v[] = sprintf('title="Max %s"', bytes_to_php(upload_max_filesize()));
        $ret[] = sprintf('<input %s>', implode(' ', $v));
-       if ($info['type'] == 'file')
-         $js_file = sprintf("  var finput = document.getElementById('edit_%s');\n" .
-                            "  if (finput) finput.addEventListener('change', form_file_change, false);",
-                            $id);
       } elseif ($info['type'] == 'select') {
        if (array_key_exists('onchange',$info))
          $v[] = sprintf('onchange="%s"', $info['onchange']);
@@ -94,30 +102,23 @@ function build_form($name, $mask)
        foreach ($options as $row)
          $ret[] = sprintf('<option value="%s"%s>%s</option>',
                           $row['id'],
-                          !empty($info['selected']) && $row['id'] == $info['selected'] ? ' selected' : '',
+                          array_key_exists('selected', $info) && $row['id'] == $info['selected'] ? ' selected' : '',
                           $row['text']);
        $ret[] = '</select>';
       } elseif ($info['type'] == 'textarea') {
-       $v[] = sprintf('cols="%d"', empty($info['columns'])?33:$info['columns']);
-       $v[] = sprintf('rows="%d"', empty($info['rows'])?5:$info['rows']);
+       $v[] = sprintf('cols="%d"', !array_key_exists('columns', $info)?33:$info['columns']);
+       $v[] = sprintf('rows="%d"', !array_key_exists('rows', $info)?5:$info['rows']);
        $ret[] = sprintf('<textarea %s></textarea>', implode(' ', $v));
       }
 
       if (array_key_exists('comment',$info))
        $ret[] = sprintf('<span class="comment">%s</span>', $info['comment']);
-      $ret[] = '<br>';
 
       if (count($checks))
-       $jscode[] = sprintf('form_check["%s"] = "%s";', $id, implode(',', $checks));
+       $form_checks[$id] = implode(',', $checks);
     }
   }
 
-  $jscode[] = 'Rico.onLoad( function() {';
-  $jscode[] = "  form_init();";
-  if (isset($js_file))
-    $jscode[] = $js_file;
-  $jscode[] = '});';
-
   $v_save = array('id="button_save"',
                  'onclick="return form_save(this);"');
   if (isset($mask['buttons']) && isset($mask['buttons']['save']) && $mask['buttons']['save'] === false) $v_save[] = 'style="display: none"';
@@ -130,7 +131,7 @@ function build_form($name, $mask)
                    'onclick="return form_delete(this);"');
   if (isset($mask['buttons']) && isset($mask['buttons']['delete']) && $mask['buttons']['delete'] === false) $v_delete[] = 'style="display: none"';
 
-  $v_changes = array('id="form_status"');
+  $v_changes = array('id="form_status"', 'title="Last modified"');
   if (isset($mask['buttons']) && isset($mask['buttons']['changes']) && $mask['buttons']['changes'] === false) $v_changes[] = 'style="display: none"';
 
   $ret[] = sprintf('<p %s>&nbsp;</p>', implode(' ', $v_changes));
@@ -145,234 +146,188 @@ function build_form($name, $mask)
   $ret[] = '</form>';
   $ret[] = '</div>';
 
-  return $ret;
+  return array('form' => implode('', $ret), 'checks' => $form_checks);;
 }
 
-function build_grid($name, $mask, $gridname = false)
+function grid_details($source, $name, $def)
 {
-  $jscode = array();;
-  $ret = array();
-
-  if ($gridname == false) {
-    $gridname = 'grid';
-    $opts = array("click: gridDrillDown");
-  } else {
-    $name = $name . '__' . $gridname;
-    $opts = array();
-    if (array_key_exists('onclick', $mask)) $opts[] = 'click: ' . $mask['onclick'];
-  }
-
-  $opts[] = "onscroll: gridOnScroll";
-  $opts[] = "menuEvent: 'contextmenu'";
-  $opts[] = "highlightElem: 'menuRow'";
-
-  if (array_key_exists('maxprint', $mask)) $opts[] = 'maxPrint: ' . $mask['maxprint'];
-  else $opts[] = 'maxPrint: 100000';
-  if (array_key_exists('rows', $mask)) $opts[] = 'visibleRows: ' . $mask['rows'];
-  if (array_key_exists('sort', $mask)) $opts[] = 'sortCol: ' . $mask['sort'];
-  if (isset($mask['prefetch']) && $mask['prefetch'] === false) $opts[] = 'prefetchBuffer: false';
-  # $opts[] = 'frozenColumns: ' . count($mask['list']);
-  $opts[] = 'saveColumnInfo: {width: true, filter: true, sort: true}';
-
-  $ret[] = sprintf('<table id="grid_%s" class="ricoLiveGrid">', $name);
-  $ret[] = '  <tr>';
-  $specs = array();
-  $fields = array();
-  foreach ($mask['list'] as $field => $data) {
-    $ret[] = sprintf('  <th>%s</th>', $data['name']);
-    $s = array(sprintf("FieldName: 'input_%s'", $field),
-              sprintf("ColName: 'input_%s'", $data['name']));
-    $s = array();
-    if (isset($data['visible']) && $data['visible'] === false) $s[] = 'visible: false';
-    if (isset($data['width']) && $data['width'] > 0) $s[] = 'width: ' . $data['width'];
-    if (array_key_exists('type', $data)) $s[] = "type: '" . $data['type'] . "'";
-    if (array_key_exists('specs', $data)) $s[] = $data['specs'];
-    if (array_key_exists('control', $data)) $s[] = 'control: ' . $data['control'];
-    $specs[] = '{' . implode(', ', $s) . '}';
-
-    if (array_key_exists('sql', $data))
-      $fields[] = $data['sql'] . ' AS ' . $field;
-    else
-      $fields[] = $field;
+  $hallinta = Hallinta::instance();
+  if ($name == 'main')
+    $gridName = $source;
+  else
+    $gridName = $source . '__' . $name . '__second';
+
+  $options = array();
+  $options['onscroll'] = 'gridOnScroll';
+  $options['menuEvent'] = 'contextmenu';
+  $options['highlightElem'] = 'menuRow';
+
+  if ($name == 'main') $options['click'] = 'gridDrillDown';
+  if (array_key_exists('onclick', $def)) $options['click'] = $def['onclick'];
+  if (array_key_exists('rows', $def)) $options['visibleRows'] = intval($def['rows']);
+  if (array_key_exists('sort', $def)) $options['sortCol'] = intval($def['sort']);
+  if (array_key_exists('sortdir', $def)) $options['sortDir'] = $def['sortdir'];
+  if (array_key_exists('prefetch', $def) && $def['prefetch'] == false) $options['prefetchBuffer'] = false;
+  if (array_key_exists('maxprint', $def)) $options['maxPrint'] = intval($def['maxprint']);
+  else $options['maxPrint'] = 100000;
+  $options['saveColumnInfo'] = array('width' => true, 'filter' => true, 'sort' => true);
+
+  $canEdit = array_key_exists('table_edit', $def);
+  if ($canEdit) {
+    $options['canEdit'] = true;
+    $options['updateURL'] = $hallinta->urlbase().'ajax/ajax.php';
   }
 
-  grid_sql($name, $mask);
-
-  $ret[] = '  </tr>';
-  $ret[] = '</table>';
-  $ret[] = '<div>';
-  $ret[] = sprintf('<div class="info" id="info_grid_%s">Datensatz 0</div>', $name);
-  if ($gridname == 'grid')
-    $ret[] = '<div class="status"><span id="status"></span></div>';
-  $ret[] = '</div>';
-  $ret[] = '<br style="clear:both;">';
-
-  $opts[] = 'columnSpecs: [' . implode(', ', $specs) . ']';
-
-  if (strstr($opts[count($opts)-1], 'filterUI') !== false) $opts[] = 'FilterLocation: -1';
+  $hasFilter = false;
+  $columnSpecs = array();
+  $html = array();
+  $html[] = sprintf('<table id="grid_%s" class="ricoLiveGrid">', $gridName);
+  $html[] = '<tr>';
+  foreach ($def['list'] as $field => $data) {
+    $html[] = sprintf('<th>%s</th>', $data['name']);
+    if ($canEdit && array_key_exists('edit', $data)) {
+      if (!is_array($data['edit']))
+       throw new Exception(sprintf('Grid %s column %s edit is not an array', $name, $data['name']));
+      $s = $data['edit'];
+      $s['FieldName'] = sprintf('listinput_%s', $field);
+      $s['ColName'] = $data['name'];
+    } else {
+      $s = array();
+    }
+    if (isset($data['visible'])) {
+       if ($data['visible'] === false)
+           $s['visible'] = false;
+       elseif ($data['visible'] === 'mobile' && !$hallinta->isMobile())
+           $s['visible'] = false;
+       elseif ($data['visible'] === 'desktop' && $hallinta->isMobile())
+           $s['visible'] = false;
+    }
+    if (isset($data['width']) && $data['width'] > 0) $s['width'] = intval($data['width']);
+    if (array_key_exists('type', $data)) $s['type'] = $data['type'];
+    if (array_key_exists('filter', $data)) {
+      $s['filterUI'] = $data['filter'];
+      $hasFilter = true;
+    }
+    if (array_key_exists('control', $data)) $s['control'] = $data['control'];
+    if (array_key_exists('specs', $data)) {
+      if (!is_array($data['specs']))
+        throw new Exception(sprintf('Grid %s column %s specs is not an array', $name, $data['name']));
+      $s = array_merge($s, $data['specs']);
+    }
 
-  $jscode[] = sprintf('var %s;', $gridname);;
-  $jscode[] = 'Rico.onLoad( function() {';
-  $jscode[] = sprintf("var %s_opts = {\n  %s\n};", $name, implode(",\n  ", $opts));
-  $jscode[] = sprintf("%s = new Rico.LiveGrid ('grid_%s', new Rico.Buffer.AjaxSQL('ajax/ricoXMLquery.php'), %s_opts);", $gridname, $name, $name);
-  $jscode[] = sprintf("%s.menu = new Rico.GridMenu();", $gridname);
-  $jscode[] = sprintf("%s.edit = new Rico.TableEdit(%s);", $gridname, $gridname);
-  if ($gridname == 'grid') {
-    $jscode[] = sprintf("%s.menu.options.dataMenuHandlerOriginal = %s.menu.options.dataMenuHandler;", $gridname, $gridname);
-    $jscode[] = sprintf("%s.menu.options.dataMenuHandler = grid_dataMenuHandler;", $gridname);
+    $columnSpecs[] = $s;
   }
-  $jscode[] = '});';
-
-  return array($ret, $jscode);
-}
-
-function build_details($name, $details)
-{
-  $ret = array();
+  $options['columnSpecs'] = $columnSpecs;
 
-  $ret[] = '<div class="box" id="details">';
-  $ret[] = sprintf('<h3>%s</h3>', $details['title']);
-  if (array_key_exists('subtitle', $details))
-    $ret[] = sprintf('<p class="subtitle">%s</p>', $details['subtitle']);
-  if (array_key_exists('html',$details)) {
-    $ret[] = $details['html'];
-  } elseif (array_key_exists('list',$details)) {
-    $ret[] = '<ul>';
-    foreach ($details['list'] as $name => $info) {
-      $ret[] = sprintf('<li>%s: <span id="detail_%s"></span></li>', $info['name'], $name);
-    }
-    $ret[] = '</ul>';
+  if ($hasFilter) {
+    $options['AutoFilter'] = true;
+    $options['FilterLocation'] = -1;
   }
-  $ret[] = '</div>';
 
-  return $ret;
-}
-
-function build_select($name, $details)
-{
-  $ret = array();
+  $html[] = '</tr>';
+  $html[] = '</table>';
 
-  $ret[] = '<div class="form">';
-  $ret[] = sprintf('<p class="title">%s</p>', $details['title']);
-  $ret[] = sprintf ('<select%s>', array_key_exists('onchange',$details)?sprintf(' onchange="%s"',$details['onchange']):'');
-  if (is_array($details['options']))
-    $options = $details['options'];
-  else
-    $options = query_db($details['options']);
+  $html[] = '<div>';
+  $html[] = sprintf('<div class="info" id="info_grid_%s">Datensatz 0</div>', $gridName);
+  if ($name == 'main') $html[] = '<div class="status"><span id="status"></span></div>';
+  $html[] = '</div>';
 
-  if (array_key_exists('default',$details))
-    $ret[] = sprintf('<option value="">%s</option>', $details['default']);
+  grid_sql($gridName, $def);
 
-  foreach ($options as $row) {
-    $selected = array_key_exists('selected',$details) && $details['selected'] == $row['id'] ? true : false;
-    $ret[] = sprintf('<option value="%s"%s>%s</option>', $row['id'], $selected?' selected':'',$row['text']);
-  }
+  $grid =  array('name' => $name,
+                'gridname' => $gridName,
+                'title' => $def['title'],
+                'html' => implode('',$html),
+                'options' => $options);
 
-  $ret[] = '</select>';
-  $ret[] = '</div>';
+  if (array_key_exists('width', $def))
+    $grid['width'] = $def['width'];
 
-  return $ret;
+  return $grid;
 }
 
-function build_mask($name, $mask)
+function build_mask($module, $page, $mask)
 {
-  global $jscode;
-  $jscode[] = 'Rico.writeDebugMsg = function(msg, resetFlag) {};';
+    if (array_key_exists('edit', $mask) && check_edit($module, $page)) {
+    JavaScript::instance()->add('Hallinta.canEdit = true;');
+    Actions::instance()->addLink(new Link(array('id' => 'btn_edit',
+                                               'icon' => Hallinta::instance()->urlbase().'images/icons/edit.png',
+                                               'title' => 'Edit dialog',
+                                               'function' => 'Hallinta.openEditDialog')));
+  }
 
   if (array_key_exists('list', $mask)) {
-    $jscode[] = "Rico.acceptLanguage('de-de,de;q=0.8,en;q=0.5,en-us;q=0.3');";
-    $jscode[] = "Rico.loadModule('Effect','Calendar','LiveGridForms','LiveGridAjax','LiveGridMenu');";
-
-    if (array_key_exists('second', $mask) && !array_key_exists('rows', $mask))
-      $mask['rows'] = "'parent'";
-
-    list($grid,$js) = build_grid($name, $mask);
-    $grid = array_merge(array('<div id="div_grid">'), $grid, array('</div>'));
+    $grid = array('<div id="div_grid">', '</div>');
     if (array_key_exists('second', $mask)) {
-      $mask['second']['prefetch'] = false;
-
-      list($second,$secondjs) = build_grid($name, $mask['second'], 'second');
-      $jscode = array_merge($jscode, $secondjs);
-
-      $grid[] = '<div id="second" class="second">';
-      $close = '<img src="images/icons/minimise.gif" title="Hide" onclick="second_toggle()" />';
-      $title = array_key_exists('title', $mask['second']) ? $mask['second']['title'] : '';
-      $grid[] = sprintf('<div style="position: relative;" id="second_title" class="ricoTitle title"><strong>%s</strong>%s</div>', $title, $close);
-
-      $grid = array_merge($grid, array('<div id="div_second">'), $second, array('</div>'));
-
-      $grid[] = '</div>';
+      foreach ($mask['second'] as $sname => $second) {
+       if (!array_key_exists('title', $second)) throw new Exception('No title for secondary grid');
+
+       JavaScript::instance()->add(sprintf("Hallinta.openSecondaryGrid_%s = function(e) { Hallinta.openSecondaryGrid(e, '%s'); } ",
+                                           $sname, $sname));
+       Actions::instance()->addLink(new Link(array('id' => 'btn_' . $sname,
+                                                   'icon' => Hallinta::instance()->urlbase().'images/icons/liste.gif',
+                                                   'title' => 'Open ' . $second['title'],
+                                                   'function' => sprintf("Hallinta.openSecondaryGrid_%s", $sname))));
+      }
     }
-    $jscode = array_merge($jscode, $js);
-    if ($mask['rows'] == "'parent'")
-      $jscode[] = 'Rico.onLoad(function() {resize_grids()});';
   } else
     $grid = array();
 
-  if (array_key_exists('details', $mask))
-    $details = build_details($name, $mask['details']);
-  else
-    $details = array();
-
-  if (array_key_exists('select', $mask))
-    $select = build_select($name, $mask['select']);
-  else
-    $select = array();
-
-  if (array_key_exists('edit', $mask) && check_edit($name))
-    $edit = build_form($name, $mask);
-  else
-    $edit = array();
-
   $title = $mask['title'];
   if (array_key_exists('subtitle', $mask))
     $title .= ' &ndash; ' . $mask['subtitle'];
+  Hallinta::instance()->setTitle($title);
 
   $head = array();
-  $head[] = sprintf('<h3 class="title">%s</h3>', $title);
 
   if (array_key_exists('html', $mask) &&
-      !array_key_exists('details', $mask) &&
-      !array_key_exists('select', $mask) &&
       !array_key_exists('edit', $mask))
     return array_merge($head,
                       array($mask['html']),
                       array('<span id="status"></span><br>'));
 
-  if (empty($details) && empty($select) && empty($edit))
+  if (empty($details))
     return array_merge($head,
                     array('<div id="column_grid" class="grid">'),
                     $grid,
                     array('</div>'));
-  else
-    return array_merge($head,
-                    array('<div id="column_grid" class="right grid">'),
-                    $grid,
-                    array('</div>','<div id="column_edit" class="left">'),
-                    $details,
-                    $select,
-                    $edit,
-                    array('</div>'));
 }
 
-function mask($name)
+function mask($module, $page)
 {
-  global $jsfiles;
   global $mask;
 
   $ret = array();
 
-  if (load_mask($name) === false)
+  $styles = Styles::instance();
+  $styles->file('lib/rico3/ricoClient/css/rico.css');
+  $styles->file('lib/rico3/ricoClient/css/rico_icon.css');
+  $styles->file('lib/rico3/ricoClient/css/striping_cupertino.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui.css');
+  $styles->file('lib/rico3/ui-cupertino/jquery-ui_hallinta.css');
+
+  $javascript = JavaScript::instance();
+  $javascript->file('lib/rico3/ricoClient/js/rico2jqu.js');
+  $javascript->file('lib/rico3/minsrc/rico.js');
+  $javascript->file('lib/rico3/minsrc/ricoGridCommon.js');
+  $javascript->file('lib/rico3/minsrc/ricoLiveGrid.js');
+  $javascript->file('lib/rico3/minsrc/ricoLiveGridForms.js');
+  $javascript->file('lib/rico3/minsrc/ricoLiveGridAjax.js');
+  $javascript->file('lib/rico3/minsrc/ricoLiveGridMenu.js');
+  $javascript->file('lib/rico3/minsrc/ricoLiveGridControls.js');
+  $javascript->file('lib/rico3/minsrc/ricoLocale_en.js');
+  $javascript->file('lib/rico3/minsrc/ricoUI.js');
+  $javascript->file('lib/rico3/ricoClient/js/ricoThemeroller.js');
+  $javascript->file('lib/rico3/minsrc/ricoCalendar.js');
+  $javascript->onLoad('page_init();');
+
+  if (load_mask($module, $page) === false)
     return;
 
-  $jsfiles[] = 'lib/functions.js';
-  $jsfiles[] = 'lib/rico/rico.js';
-
-  if (array_key_exists('table',$mask))
-    $ret = build_mask($name, $mask);
+  if (array_key_exists('table',$mask) || array_key_exists('html',$mask))
+    $ret = build_mask($module, $page, $mask);
 
-  return sprintf('<span id="source" style="display: none;">%s</span>', $_GET['mask'])
-    . implode("\n", $ret);
+  $javascript->add(sprintf('Hallinta.pageSource = "%s__%s";', $module, $page));
+  $javascript->add(sprintf('Hallinta.isMobile = %s;', Hallinta::instance()->isMobile()?'true':'false'));
+  return implode("\n", $ret);
 }
-
-?>
diff --git a/lib/menu.php b/lib/menu.php
deleted file mode 100644 (file)
index 740d60d..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-# Menu from http://lwis.net/free-css-drop-down-menu/dropdown.simple.horizontal.html
-
-function menu()
-{
-  global $db;
-
-  if (empty($_SESSION['sys']['login'])) return '';
-
-  $ret = '<div class="menu">';
-
-  $ret .= '<ul id="nav" class="dropdown dropdown-horizontal">';
-
-  $ret .= '<li class="dir">Home';
-  $ret .= '<ul>';
-  $ret .= '<li><a href="./" title="Zurück zur Hauptseite">Start</a></li>';
-  $ret .= '<li><a href="./?logout=true" title="Abmelden">Logout</a></li>';
-
-  $sql = "SELECT fname,menutitle,sys_menu.name,title FROM sys_mask "
-    . "JOIN sys_menu ON sys_mask.menu = sys_menu.id "
-    . "JOIN sys_group_mask ON sys_mask.id = sys_group_mask.mask "
-    . "WHERE sys_group_mask.gid = ".$_SESSION['sys']['group']." "
-    . "AND shadow = 0 "
-    . "ORDER BY sys_menu.priority,sys_menu.name,sys_mask.priority,sys_mask.menutitle";
-  $list = $db->fetchObjectList($sql);
-
-  if (count($list)) {
-    $menu = 'Start';
-
-    foreach ($list as $row) {
-      if ($menu != $row->name) {
-       $ret .= '</ul></li>';
-       $ret .= sprintf('<li class="dir">%s', $row->name);
-       $ret .= '<ul>';
-       $menu = $row->name;
-      }
-      $ret .= sprintf('<li><a href="./?mask=%s"%s>%s</a></li>', str_replace('|','__',$row->fname),
-                     empty($row->title) ? '' : ' title="'.$row->title.'"',
-                     $row->menutitle);
-    }
-    $ret .= '</ul>';
-    $ret .= '</li>';
-  }
-
-  $ret .= '</ul>';
-  $ret .= '</div>';
-
-  return $ret;
-}
-
-?>
\ No newline at end of file
index 6b6a6a3..f41458d 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-$jscode[] = <<<EOC
+$jscode = <<<EOC
 
 function select_year_calback(data)
 {
@@ -17,6 +17,8 @@ function select_year_status(obj, value, html)
 }
 EOC;
 
+$javascript->add($jscode);
+
 function discover_years()
 {
   if (!is_array($_SESSION['years'])) {
diff --git a/lib/php.php b/lib/php.php
new file mode 100644 (file)
index 0000000..d4562f0
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+function php_to_bytes($value)
+{
+  $value = trim($value);
+  if (is_numeric($value)) {
+    return $value;
+  } else {
+    $value_length = strlen($value);
+    $qty = substr($value, 0, $value_length - 1);
+    $unit = strtolower(substr($value, $value_length - 1));
+    switch ($unit) {
+    case 'k':
+      $qty *= 1024;
+      break;
+    case 'm':
+      $qty *= 1048576;
+      break;
+    case 'g':
+      $qty *= 1073741824;
+      break;
+    }
+    return $qty;
+  }
+}
+
+function php_round($qty)
+{
+  if ($qty - intval($qty) != 0)
+    $qty = sprintf('%.2f', $qty);
+  return $qty;
+}
+
+function bytes_to_php($value)
+{
+  if ($value > 1073741824)
+    return php_round($value / 1073741824) . 'GB';
+  elseif ($value > 1048576)
+    return php_round($value / 1048576) . 'MB';
+  elseif ($value > 1024)
+    return php_round($value / 1024) . 'KB';
+  else
+    return $value . 'B';
+}
+
+function upload_max_filesize($limit=false)
+{
+  $values = array(php_to_bytes(ini_get('upload_max_filesize')),
+                 php_to_bytes(ini_get('post_max_size')),
+                 php_to_bytes(ini_get('memory_limit')));
+  if ($limit) $values[] = $limit;
+
+  return min($values);
+}
+
+?>
index 43f34ce..6dfc910 100644 (file)
@@ -115,7 +115,8 @@ getContentAsString: function( parentNode, isEncoded ) {
 _getEncodedContent: function(parentNode) {
 
   if (parentNode.innerHTML) {
-    if (Prototype.Browser.Gecko && navigator.productSub >= "20100101")
+    if ((Prototype.Browser.Gecko && navigator.productSub >= "20100101") ||
+         Prototype.Browser.WebKit)
       parentNode.innerHTML.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
     else
       return parentNode.innerHTML;
@@ -327,11 +328,12 @@ DOMNode_insertAfter: function(newChild,refChild) {
 positionCtlOverIcon: function(ctl,icon) {
   var offsets=Position.page(icon);
   var scrTop=this.docScrollTop();
+  var scrLeft=this.docScrollLeft();
   var winHt=this.windowHeight();
   if (ctl.style.display=='none') ctl.style.display='block';
   var correction=Prototype.Browser.IE ? 1 : 2;  // based on a 1px border
   var lpad=this.nan2zero(Element.getStyle(icon,'padding-left'));
-  ctl.style.left = (offsets[0]+lpad+correction)+'px';
+  ctl.style.left = (offsets[0]+lpad+correction+scrLeft)+'px';
   var newTop=offsets[1] + correction + scrTop;
   var ctlht=ctl.offsetHeight;
   var iconht=icon.offsetHeight;
index 855245a..69e9b7f 100644 (file)
@@ -782,6 +782,18 @@ Rico.LiveGridMethods.prototype = {
     //var colnum = rowsElement.getAttribute("distinct");
     var col=this.columns[parseInt(colnum,10)];
     var rows = this.buffer.dom2jstable(rowsElement);
+    var found = !col.filterValues || !col.filterValues.length;
+    if (!found) for (var i=0; i<rows.length; i++) {
+       if (rows[i][0] == col.filterValues[0]) {
+           found = true;
+           break;
+       }
+    }
+    if (!found) {
+       col.setUnfiltered(true);
+       if (this.options.filterHandler)
+           this.options.filterHandler();
+    }
     var c0,c1,opt,v, field=$(this.filterId(colnum));
     if (col.filterType==Rico.TableColumn.USERFILTER && col.filterOp=='EQ') v=col.filterValues[0];
     Rico.writeDebugMsg('filterValuesUpdate: col='+colnum+' rows='+rows.length);
@@ -792,6 +804,7 @@ Rico.LiveGridMethods.prototype = {
           c1=RegExp.leftContext;
         }
         if (col._getdesc) c1 = col._getdesc(c1);
+        if (c1 == '&nbsp;') c1 = '';
         opt=RicoUtil.addSelectOption(field,c0,c1 || RicoTranslate.getPhraseById("filterBlank"));
         if (col.filterType==Rico.TableColumn.USERFILTER && c0==v) opt.selected=true;
       }
index 2b12135..ace80ef 100644 (file)
@@ -605,7 +605,7 @@ Rico.TableEdit = Class.create(
   processResponse: function() {
     var responseText,success=true;
     var respNodes=Element.select(this.responseDiv,'.ricoFormResponse');
-    if (respNodes) {
+    if (respNodes && respNodes.length) {
       // generate a translated response
       var phraseId=$w(respNodes[0].className)[1];
       responseText=RicoTranslate.getPhraseById(phraseId,this.options.RecordName);
index be2f100..4a3d8ef 100644 (file)
@@ -340,7 +340,22 @@ class ricoXmlResponse {
           //print_r($value);
           foreach($value as $i => $filter) {
             if ($i<0 || $i>=count($this->oParse->arSelList)) break;
-            $newfilter=$this->oParse->arSelList[$i];
+           switch ($this->objDB->Dialect) {
+           case "PostgreSQL":
+             global $mask;
+             if ($filter['op'] == 'LIKE' &&
+                 array_key_exists($this->oParse->arSelList[$i], $mask['list']) &&
+                 array_key_exists('sqltype', $mask['list'][$this->oParse->arSelList[$i]]) &&
+                 in_array($mask['list'][$this->oParse->arSelList[$i]]['sqltype'], array('date','int'))) {
+               $newfilter=sprintf('cast(%s as varchar(100))', $this->oParse->arSelList[$i]);
+             } else
+               $newfilter=$this->oParse->arSelList[$i];
+             break;
+           default:
+             $newfilter=$this->oParse->arSelList[$i];
+             break;
+           }
+
             $this->setCondType($newfilter);
             switch ($filter['op']) {
               case "EQ":
diff --git a/lib/rico/translations/ricoLocale_ua.js b/lib/rico/translations/ricoLocale_ua.js
deleted file mode 100644 (file)
index 593cd28..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/*****************************************************************
- Page : ricoLocale_ua.js
- Description : ukrainian localization strings
- Version 0.1 (revisions by Alexey Uvarov,Illiya Gannitskiy)
- If you would like to include translations for another language, 
- please send them to dowdybrown@yahoo.com
-******************************************************************/
-RicoTranslate.langCode='ua';
-
-// used in ricoLiveGrid.js
-
-RicoTranslate.addPhraseId('bookmarkExact',"Перегляд записів $1 - $2 з $3");
-RicoTranslate.addPhraseId('bookmarkAbout',"Перегляд записів $1 - $2 з більш ніж $3");
-RicoTranslate.addPhraseId('bookmarkNoRec',"Немає записів");
-RicoTranslate.addPhraseId('bookmarkNoMatch',"Немає збігів");
-RicoTranslate.addPhraseId('bookmarkLoading',"Завантаження...");
-RicoTranslate.addPhraseId('sorting',"Сортування...");
-RicoTranslate.addPhraseId('exportStatus',"Експортується запис $1");
-RicoTranslate.addPhraseId('filterAll',"(всі)");
-RicoTranslate.addPhraseId('filterBlank',"(чистий)");
-RicoTranslate.addPhraseId('filterEmpty',"(порожній)");
-RicoTranslate.addPhraseId('filterNotEmpty',"(не порожній)");
-RicoTranslate.addPhraseId('filterLike',"як: $1");
-RicoTranslate.addPhraseId('filterNot',"не: $1");
-RicoTranslate.addPhraseId('requestError',"Запит даних повернув помилку:\n$1");
-RicoTranslate.addPhraseId('keywordPrompt',"Шукати по ключу (Використовуйте * для всіх записів):");
-
-// used in ricoLiveGridMenu.js
-
-RicoTranslate.addPhraseId('gridmenuSortBy',"Сортування по: $1");
-RicoTranslate.addPhraseId('gridmenuSortAsc',"Зростаюча");
-RicoTranslate.addPhraseId('gridmenuSortDesc',"Убутна");
-RicoTranslate.addPhraseId('gridmenuFilterBy',"Фільтрація по: $1");
-RicoTranslate.addPhraseId('gridmenuRefresh',"Обновити");
-RicoTranslate.addPhraseId('gridmenuChgKeyword',"Змінити ключове слово...");
-RicoTranslate.addPhraseId('gridmenuExcludeAlso',"Виключити також це значення");
-RicoTranslate.addPhraseId('gridmenuInclude',"Включити тільки це значення");
-RicoTranslate.addPhraseId('gridmenuGreaterThan',"Більше або дорівнює даному значенню");
-RicoTranslate.addPhraseId('gridmenuLessThan',"Менше або дорівнює даному значенню");
-RicoTranslate.addPhraseId('gridmenuContains',"Містить значення...");
-RicoTranslate.addPhraseId('gridmenuExclude',"Виключити це значення");
-RicoTranslate.addPhraseId('gridmenuRemoveFilter',"Вилучити фільтр");
-RicoTranslate.addPhraseId('gridmenuRemoveAll',"Вилучити всі фільтри");
-
-RicoTranslate.addPhraseId('gridmenuExport',"Друк/Експорт"");
-RicoTranslate.addPhraseId('gridmenuExportVis2Web',"Видимі записи на веб-сторінку");
-RicoTranslate.addPhraseId('gridmenuExportAll2Web',"Усі записи на веб-сторінку");
-RicoTranslate.addPhraseId('gridmenuExportVis2SS',"Видимі записи в аркуш excel");
-RicoTranslate.addPhraseId('gridmenuExportAll2SS',"Усі записи в аркуш excel");
-
-RicoTranslate.addPhraseId('gridmenuHideShow',"Сховати/Показати");
-RicoTranslate.addPhraseId('gridmenuChooseCols',"Виберіть колонку...");
-RicoTranslate.addPhraseId('gridmenuHide',"Сховати: $1");
-RicoTranslate.addPhraseId('gridmenuShow',"Показати: $1");
-RicoTranslate.addPhraseId('gridmenuShowAll',"Показати всі");
-
-// used in ricoLiveGridAjax.js
-
-RicoTranslate.addPhraseId('sessionExpireMinutes',"хвилин до закінчення сесії");
-RicoTranslate.addPhraseId('sessionExpired',"МИНУЛА");
-RicoTranslate.addPhraseId('requestTimedOut',"Перевищений інтервал очікування даних!");
-RicoTranslate.addPhraseId('waitForData',"Очікування даних...");
-RicoTranslate.addPhraseId('httpError',"Отримана HTTP помилка: $1");
-RicoTranslate.addPhraseId('invalidResponse',"Сервер повернув неправильну відповідь");
-
-// used in ricoLiveGridCommon.js
-
-RicoTranslate.addPhraseId('gridChooseCols',"Вибрати колонку");
-RicoTranslate.addPhraseId('exportComplete',"Експорт завершений");
-RicoTranslate.addPhraseId('exportInProgress',"Експортування...");
-RicoTranslate.addPhraseId('showFilterRow',"Показати відфільтровані записи");  // img alt text
-RicoTranslate.addPhraseId('hideFilterRow',"Сховати відфільтровані записи");  // img alt text
-
-// used in ricoLiveGridForms.js
-
-RicoTranslate.addPhraseId('selectNone',"(нічого)");
-RicoTranslate.addPhraseId('selectNewVal',"(нове значення)");
-RicoTranslate.addPhraseId('record',"запис");
-RicoTranslate.addPhraseId('thisRecord',"ця $1");
-RicoTranslate.addPhraseId('confirmDelete',"Ви впевнені,що бажаєте видалити $1?");
-RicoTranslate.addPhraseId('deleting',"Видалення...");
-RicoTranslate.addPhraseId('formPleaseEnter',"Будь ласка, введіть значення для $1");
-RicoTranslate.addPhraseId('formInvalidFmt',"Неправильний формат для $1");
-RicoTranslate.addPhraseId('formOutOfRange',"Значення знаходиться поза діапазоном для $1");
-RicoTranslate.addPhraseId('formNewValue',"нове значення:");
-RicoTranslate.addPhraseId('saving',"Збереження...");
-RicoTranslate.addPhraseId('clear',"очистити");
-RicoTranslate.addPhraseId('close',"Закрити");
-RicoTranslate.addPhraseId('saveRecord',"Зберегти $1");
-RicoTranslate.addPhraseId('cancel',"Скасування");
-RicoTranslate.addPhraseId('editRecord',"Редагувати цю $1");
-RicoTranslate.addPhraseId('deleteRecord',"Вилучити цю $1");
-RicoTranslate.addPhraseId('cloneRecord',"Копіювати цю $1");
-RicoTranslate.addPhraseId('addRecord',"Додати нову $1");
-RicoTranslate.addPhraseId('addedSuccessfully',"$1 додана успішно");
-RicoTranslate.addPhraseId('deletedSuccessfully',"$1 вилучена успішно");
-RicoTranslate.addPhraseId('updatedSuccessfully',"$1 оновлена успішно");
-
-// used in ricoTree.js
-
-RicoTranslate.addPhraseId('treeSave',"Зберегти виділення");
-RicoTranslate.addPhraseId('treeClear',"Очистити все");
-
-// used in ricoCalendar.js
-
-RicoTranslate.addPhraseId('calToday',"Сьогодні $1 $2 $3");  // $1=day, $2=monthabbr, $3=year, $4=month number
-RicoTranslate.addPhraseId('calWeekHdg',"Тд");
-RicoTranslate.addPhraseId('calYearRange',"Рік ($1-$2)");
-RicoTranslate.addPhraseId('calInvalidYear',"Неправильний рік");
-
-// Date & number formats
-
-RicoTranslate.thouSep=","
-RicoTranslate.decPoint="."
-RicoTranslate.dateFmt="dd/mm/yyyy"
-
-RicoTranslate.monthNames=['Січень','Лютий','Березень','Квітень','Травень','Червень','Липень','Серпень','Вересень','Жовтень','Листопад','Грудень']
-RicoTranslate.dayNames=['Неділя','Понеділок','Вівторок','Середа','Четвер','П'ятниця','Субота']
diff --git a/lib/rico3/LICENSE b/lib/rico3/LICENSE
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/lib/rico3/minsrc/rico.js b/lib/rico3/minsrc/rico.js
new file mode 100644 (file)
index 0000000..4ffdd60
--- /dev/null
@@ -0,0 +1,772 @@
+/*
+ *  (c) 2005-2012 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2012 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+Rico.Version='3.0';
+Rico.theme={};
+Rico.onLoadCallbacks=[];
+Rico.windowIsLoaded=false;
+Rico.inputtypes={search: 0, number: 0, range: 0, color: 0, tel: 0, url: 0, email: 0, date: 0, month: 0, week: 0, time: 0, datetime: 0, 'datetime-local': 0};
+
+// called by the document onload event
+Rico.windowLoaded=function() {
+  this.windowIsLoaded=true;
+  if (typeof Rico_CONFIG == 'object') {
+    if (Rico_CONFIG.enableLogging) this.enableLogging();
+    if (Rico_CONFIG.enableHTML5) this._CheckInputTypes();
+  }
+  Rico.writeDebugMsg=Rico.log;  // for backwards compatibility
+  Rico.log('Processing callbacks');
+  while (this.onLoadCallbacks.length > 0) {
+    var callback=this.onLoadCallbacks.shift();
+    if (callback) callback();
+  }
+};
+
+// check for availability of HTML5 input types
+Rico._CheckInputTypes=function() {
+  var i = document.createElement("input");
+  for (var itype in this.inputtypes) {
+    i.setAttribute("type", "text");
+    i.setAttribute("type", itype);
+    this.inputtypes[itype]=(i.type !== "text");
+  }
+};
+
+Rico.onLoad=function(callback,frontOfQ) {
+  if (this.windowIsLoaded)
+    callback();
+  else if (frontOfQ)
+    this.onLoadCallbacks.unshift(callback);
+  else
+    this.onLoadCallbacks.push(callback);
+};
+
+Rico.isKonqueror=navigator.userAgent.toLowerCase().indexOf("konqueror") > -1;
+Rico.isIE=!!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1);
+Rico.isOpera=navigator.userAgent.indexOf('Opera') > -1;
+Rico.isWebKit=navigator.userAgent.indexOf('AppleWebKit/') > -1;
+Rico.isGecko=navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1;
+Rico.ieVersion=/MSIE (\d+\.\d+);/.test(navigator.userAgent) ? new Number(RegExp.$1) : null;
+
+// logging funtions
+
+Rico.startTime=new Date();
+
+Rico.timeStamp=function() {
+  var stamp = new Date();
+  return (stamp.getTime()-this.startTime.getTime())+": ";
+};
+
+Rico.setDebugArea=function(id, forceit) {
+  if (!this.debugArea || forceit) {
+    var newarea=document.getElementById(id);
+    if (!newarea) return;
+    this.debugArea=newarea;
+    newarea.value='';
+  }
+};
+
+Rico.log=function() {};
+
+Rico.enableLogging=function() {
+  if (this.debugArea) {
+    this.log = function(msg, resetFlag) {
+      if (resetFlag) this.debugArea.value='';
+      this.debugArea.value+=this.timeStamp()+msg+"\n";
+    };
+  } else if (window.console) {
+    if (window.console.firebug)
+      this.log = function(msg) { window.console.log(this.timeStamp(),msg); };
+    else
+      this.log = function(msg) { window.console.log(this.timeStamp()+msg); };\r
+  } else if (window.opera) {
+    this.log = function(msg) { window.opera.postError(this.timeStamp()+msg); };
+  }
+};
+
+Rico.$=function(e) {
+  return typeof e == 'string' ? document.getElementById(e) : e;
+};
+
+Rico.runLater=function() {
+  var args = Array.prototype.slice.call(arguments);
+  var msec = args.shift();
+  var object = args.shift();
+  var method = args.shift();
+  return setTimeout(function() { object[method].apply(object,args); },msec);
+};
+
+Rico.visible=function(element) {
+  return Rico.getStyle(element,"display") != 'none';
+};
+
+Rico.show=function(element) {
+  element.style.display = '';
+};
+
+Rico.hide=function(element) {
+  element.style.display = 'none';
+};
+
+Rico.toggle=function(element) {
+  element.style.display = element.style.display == 'none' ? '' : 'none';
+};
+
+// ltr or rtl
+Rico.direction=function(element) {
+  return (Rico.getStyle(element,'direction') || 'ltr').toLowerCase();
+};
+
+Rico.viewportOffset=function(element) {
+  var offset=Rico.cumulativeOffset(element);
+  offset.left -= this.docScrollLeft();
+  offset.top -= this.docScrollTop();
+  return offset;
+};
+
+/**
+ * Return text within an html element
+ * @param el DOM node
+ * @param xImg true to exclude img tag info
+ * @param xForm true to exclude input, select, and textarea tags
+ * @param xClass exclude elements with a class name of xClass
+ */
+Rico.getInnerText=function(el,xImg,xForm,xClass) {
+  switch (typeof el) {
+    case 'string': return el;
+    case 'undefined': return el;
+    case 'number': return el.toString();
+  }
+  var cs = el.childNodes;
+  var l = cs.length;
+  var str = "";
+  for (var i = 0; i < l; i++) {
+    switch (cs[i].nodeType) {
+    case 1: //ELEMENT_NODE
+      if (this.getStyle(cs[i],'display')=='none') continue;
+      if (xClass && this.hasClass(cs[i],xClass)) continue;
+      switch (cs[i].tagName.toLowerCase()) {
+        case 'img':   if (!xImg) str += cs[i].alt || cs[i].title || cs[i].src; break;
+        case 'input': if (!xForm && !cs[i].disabled && cs[i].type.toLowerCase()=='text') str += cs[i].value; break;
+        case 'select': if (!xForm && cs[i].selectedIndex>=0) str += cs[i].options[cs[i].selectedIndex].text; break;
+        case 'textarea': if (!xForm && !cs[i].disabled) str += cs[i].value; break;
+        default:      str += this.getInnerText(cs[i],xImg,xForm,xClass); break;
+      }
+      break;
+    case 3: //TEXT_NODE
+      str += cs[i].nodeValue;
+      break;
+    }
+  }
+  return str;
+};
+
+/**
+ * Return value of a node in an XML response.
+ * For Konqueror 3.5, isEncoded must be true.
+ */
+Rico.getContentAsString=function( parentNode, isEncoded ) {
+  if (isEncoded) return this._getEncodedContent(parentNode);
+  if (typeof parentNode.xml != 'undefined') return this._getContentAsStringIE(parentNode);
+  return this._getContentAsStringMozilla(parentNode);
+};
+
+Rico._getEncodedContent=function(parentNode) {
+  if (parentNode.innerHTML) {
+    if (Rico.isGecko && navigator.productSub >= "20100101" || Rico.isWebKit)
+      parentNode.innerHTML.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
+    else
+      return parentNode.innerHTML;
+  }
+
+  switch (parentNode.childNodes.length) {
+    case 0:  return "";
+    case 1:  return parentNode.firstChild.nodeValue;
+    default: return parentNode.childNodes[1].nodeValue;
+  }
+};
+
+Rico._getContentAsStringIE=function(parentNode) {
+  var contentStr = "";
+  for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
+     var n = parentNode.childNodes[i];
+     contentStr += (n.nodeType == 4) ? n.nodeValue : n.xml;
+  }
+  return contentStr;
+};
+
+Rico._getContentAsStringMozilla=function(parentNode) {
+   var xmlSerializer = new XMLSerializer();
+   var contentStr = "";
+   for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) {
+        var n = parentNode.childNodes[i];
+        if (n.nodeType == 4) { // CDATA node
+            contentStr += n.nodeValue;
+        }
+        else {
+          contentStr += xmlSerializer.serializeToString(n);
+      }
+   }
+   return contentStr;
+};
+
+/**
+ * @param n a number (or a string to be converted using parseInt)
+ * @returns the integer value of n, or 0 if n is not a number
+ */
+Rico.nan2zero=function(n) {
+  if (typeof(n)=='string') n=parseInt(n,10);
+  return isNaN(n) || typeof(n)=='undefined' ? 0 : n;
+};
+
+Rico.stripTags=function(s) {
+  return s.replace(/<\/?[^>]+>/gi, '');
+};
+
+Rico.truncate=function(s,length) {
+  return s.length > length ? s.substr(0, length - 3) + '...' : s;
+};
+
+Rico.zFill=function(n,slen, radix) {
+  var s=n.toString(radix || 10);
+  while (s.length<slen) s='0'+s;
+  return s;
+};
+
+Rico.keys=function(obj) {
+  var objkeys=[];
+  for(var k in obj)
+    objkeys.push(k);
+  return objkeys;
+};
+
+/**
+ * @param e event object
+ * @returns the key code stored in the event
+ */
+Rico.eventKey=function(e) {
+  if( typeof( e.keyCode ) == 'number'  ) {
+    return e.keyCode; //DOM
+  } else if( typeof( e.which ) == 'number' ) {
+    return e.which;   //NS 4 compatible
+  } else if( typeof( e.charCode ) == 'number'  ) {
+    return e.charCode; //also NS 6+, Mozilla 0.9+
+  }
+  return -1;  //total failure, we have no way of obtaining the key code
+};
+
+Rico.eventLeftClick=function(e) {
+  return (((e.which) && (e.which == 1)) ||
+          ((e.button) && (e.button == 1)));
+};
+
+Rico.eventRelatedTarget=function(e) {
+  return e.relatedTarget;
+};
+
+  /**
+ * Return the previous sibling that has the specified tagName
+ */
+ Rico.getPreviosSiblingByTagName=function(el,tagName) {
+   var sib=el.previousSibling;
+   while (sib) {
+     if ((sib.tagName==tagName) && (sib.style.display!='none')) return sib;
+     sib=sib.previousSibling;
+   }
+   return null;
+ };
+
+/**
+ * Return the parent of el that has the specified tagName.
+ * @param el DOM node
+ * @param tagName tag to search for
+ * @param className optional
+ */
+Rico.getParentByTagName=function(el,tagName,className) {
+  var par=el;
+  tagName=tagName.toLowerCase();
+  while (par) {
+    if (par.tagName && par.tagName.toLowerCase()==tagName) {
+      if (!className || par.className.indexOf(className)>=0) return par;
+    }
+       par=par.parentNode;
+  }
+  return null;
+};
+
+/**
+ * Wrap the children of a DOM element in a new element
+ * @param el the element whose children are to be wrapped
+ * @param cls class name of the wrapper (optional)
+ * @param id id of the wrapper (optional)
+ * @param wrapperTag type of wrapper element to be created (optional, defaults to DIV)
+ * @returns new wrapper element
+ */
+Rico.wrapChildren=function(el,cls,id,wrapperTag) {
+  var wrapper = document.createElement(wrapperTag || 'div');
+  if (id) wrapper.id=id;
+  if (cls) wrapper.className=cls;
+  while (el.firstChild) {
+    wrapper.appendChild(el.firstChild);
+  }
+  el.appendChild(wrapper);
+  return wrapper;
+};
+
+/**
+ * Positions ctl over icon
+ * @param ctl (div with position:absolute)
+ * @param icon element (img, button, etc) that ctl should be displayed next to
+ */
+Rico.positionCtlOverIcon=function(ctl,icon) {
+  icon=this.$(icon);
+  var offsets=this.cumulativeOffset(icon);
+  var scrTop=this.docScrollTop();
+  var scrLeft=this.docScrollLeft();
+  var winHt=this.windowHeight();
+  if (ctl.style.display=='none') ctl.style.display='block';
+  var correction=2;  // based on a 1px border
+  if (Rico.direction(icon) == 'rtl') {
+    //var margin=this.nan2zero(this.getStyle(icon,'marginRight'));
+    ctl.style.left = (offsets.left + icon.offsetWidth - ctl.offsetWidth)+'px';
+  } else {
+    var margin=this.nan2zero(this.getStyle(icon,'marginLeft'));
+    ctl.style.left = (offsets.left+margin+correction+scrLeft)+'px';
+  }
+  var newTop=offsets.top + correction;// + scrTop;
+  var ctlht=ctl.offsetHeight;
+  var iconht=icon.offsetHeight;
+  var margin=10;  // account for shadow
+  if (newTop+iconht+ctlht+margin < winHt+scrTop) {
+    newTop+=iconht;  // display below icon
+  } else {
+    newTop=Math.max(newTop-ctlht,scrTop);  // display above icon
+  }
+  ctl.style.top = newTop+'px';
+};
+
+/**
+ * Creates a form element
+ * @param parent new element will be appended to this node
+ * @param elemTag element to be created (input, button, select, textarea, ...)
+ * @param elemType for input tag this specifies the type (checkbox, radio, text, ...)
+ * @param id id for new element
+ * @param name name for new element, if not specified then name will be the same as the id
+ * @returns new element
+ */
+Rico.createFormField=function(parent,elemTag,elemType,id,name) {
+  var field;
+  if (typeof name!='string') name=id;
+  if (this.isIE && this.ieVersion < 8) {
+    // IE cannot set NAME attribute on dynamically created elements
+    var s=elemTag+' id="'+id+'"';
+    if (elemType) {
+      s+=' type="'+elemType+'"';
+    }
+    if (elemTag.match(/^(form|input|select|textarea|object|button|img)$/)) {
+      s+=' name="'+name+'"';
+    }
+    field=document.createElement('<'+s+' />');
+  } else {
+    field=document.createElement(elemTag);
+    if (elemType) {
+      field.type=elemType;
+    }
+    field.id=id;
+    if (typeof field.name=='string') {
+      field.name=name;
+    }
+  }
+  parent.appendChild(field);
+  return field;
+};
+
+/**
+ * Adds a new option to the end of a select list
+ * @returns new option element
+ */
+Rico.addSelectOption=function(elem,value,text) {
+  var opt=document.createElement('option');
+  if (typeof value=='string') opt.value=value;
+  opt.text=text;
+  if (this.isIE) {
+    elem.add(opt);
+  } else {
+    elem.add(opt,null);
+  }
+  return opt;
+};
+
+/**
+ * @returns the value of the specified cookie (or null if it doesn't exist)
+ */
+Rico.getCookie=function(itemName) {
+  var arg = itemName+'=';
+  var alen = arg.length;
+  var clen = document.cookie.length;
+  var i = 0;
+  while (i < clen) {
+    var j = i + alen;
+    if (document.cookie.substring(i, j) == arg) {
+      var endstr = document.cookie.indexOf (';', j);
+      if (endstr == -1) {
+        endstr=document.cookie.length;
+      }
+      return unescape(document.cookie.substring(j, endstr));
+    }
+    i = document.cookie.indexOf(' ', i) + 1;
+    if (i == 0) break;
+  }
+  return null;
+};
+
+Rico.getTBody=function(tab) {
+  return tab.tBodies.length==0 ? tab.appendChild(document.createElement("tbody")) : tab.tBodies[0];
+};
+
+/**
+ * Write information to a cookie.
+ * For cookies to be retained for the current session only, set daysToKeep=null.
+ * To erase a cookie, pass a negative daysToKeep value.
+ * @see <a href="http://www.quirksmode.org/js/cookies.html">Quirksmode article</a> for more information about cookies.
+ */
+Rico.setCookie=function(itemName,itemValue,daysToKeep,cookiePath,cookieDomain) {
+  var c = itemName+"="+escape(itemValue);
+  if (typeof(daysToKeep)=='number') {
+    var date = new Date();
+    date.setTime(date.getTime()+(daysToKeep*24*60*60*1000));
+    c+="; expires="+date.toGMTString();
+  }
+  if (typeof(cookiePath)=='string') {
+    c+="; path="+cookiePath;
+  }
+  if (typeof(cookieDomain)=='string') {
+    c+="; domain="+cookieDomain;
+  }
+  document.cookie = c;
+};
+
+Rico.phrasesById = {};
+/** thousands separator for number formatting */
+Rico.thouSep = ",";
+/** decimal point for number formatting */
+Rico.decPoint = ".";
+/** target language (2 character code) */
+Rico.langCode = "en";
+/** date format */
+Rico.dateFmt = "mm/dd/yyyy";
+/** time format */
+Rico.timeFmt = "hh:nn:ss a/pm";
+/** month name array (Jan is at index 0) */
+Rico.monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+/** day of week array (Sunday is at index 0) */
+Rico.dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
+
+/**
+ * @param monthIdx 0-11
+ * @returns month abbreviation
+ */
+Rico.monthAbbr=function(monthIdx) {
+  return this.monthNamesShort ? this.monthNamesShort[monthIdx] : this.monthNames[monthIdx].substr(0,3);
+};
+
+/**
+ * @param dayIdx 0-6 (Sunday=0)
+ * @returns day of week abbreviation
+ */
+Rico.dayAbbr=function(dayIdx) {
+  return this.dayNamesShort ? this.dayNamesShort[dayIdx] : this.dayNames[dayIdx].substr(0,3);
+};
+
+Rico.addPhraseId=function(phraseId, phrase) {
+  this.phrasesById[phraseId]=phrase;
+};
+
+Rico.getPhraseById=function(phraseId) {
+  var phrase=this.phrasesById[phraseId];
+  if (!phrase) {
+    alert('Error: missing phrase for '+phraseId);
+    return '';
+  }
+  if (arguments.length <= 1) return phrase;
+  var a=arguments;
+  return phrase.replace(/(\$\d)/g,
+    function($1) {
+      var idx=parseInt($1.charAt(1),10);
+      return (idx < a.length) ? a[idx] : '';
+    }
+  );
+};
+
+/**
+ * Format a positive number (integer or float)
+ * @param posnum number to format
+ * @param decPlaces the number of digits to display after the decimal point
+ * @param thouSep boolean indicating whether to insert thousands separator
+ * @returns formatted string
+ */
+Rico.formatPosNumber=function(posnum,decPlaces,thouSep) {
+  var a=posnum.toFixed(decPlaces).split(/\./);
+  if (thouSep) {
+    var rgx = /(\d+)(\d{3})/;
+    while (rgx.test(a[0])) {
+      a[0]=a[0].replace(rgx, '$1'+Rico.thouSep+'$2');
+    }
+  }
+  return a.join(Rico.decPoint);
+};
+
+/**
+ * Format a number according to the specs in fmt object.
+ * @returns string, wrapped in a span element with a class of: negNumber, zeroNumber, posNumber
+ * These classes can be set in CSS to display negative numbers in red, for example.
+ *
+ * @param n number to be formatted
+ * @param fmt may contain any of the following:<dl>
+ *   <dt>multiplier </dt><dd> the original number is multiplied by this amount before formatting</dd>
+ *   <dt>decPlaces  </dt><dd> number of digits to the right of the decimal point</dd>
+ *   <dt>thouSep    </dt><dd> boolean indicating whether to insert thousands separator</dd>
+ *   <dt>prefix     </dt><dd> string added to the beginning of the result (e.g. a currency symbol)</dd>
+ *   <dt>suffix     </dt><dd> string added to the end of the result (e.g. % symbol)</dd>
+ *   <dt>negSign    </dt><dd> specifies format for negative numbers: L=leading minus, T=trailing minus, P=parens</dd>
+ *</dl>
+ */
+Rico.formatNumber=function(n,fmt) {
+  if (typeof n=='string') n=parseFloat(n.replace(/,/,'.'),10);
+  if (isNaN(n)) return 'NaN';
+  if (typeof fmt.multiplier=='number') n*=fmt.multiplier;
+  var decPlaces=typeof fmt.decPlaces=='number' ? fmt.decPlaces : 0;
+  var thouSep=typeof fmt.thouSep=='undefined' ? true : fmt.thouSep;
+  var prefix=fmt.prefix || "";
+  var suffix=fmt.suffix || "";
+  var negSign=typeof fmt.negSign=='string' ? fmt.negSign : "L";
+  negSign=negSign.toUpperCase();
+  var s,cls;
+  if (n<0.0) {
+    s=this.formatPosNumber(-n,decPlaces,thouSep);
+    if (negSign=="P") s="("+s+")";
+    s=prefix+s;
+    if (negSign=="L") s="-"+s;
+    if (negSign=="T") s+="-";
+    cls='negNumber';
+  } else {
+    cls=n==0.0 ? 'zeroNumber' : 'posNumber';
+    s=prefix+this.formatPosNumber(n,decPlaces,thouSep);
+  }
+  return "<span class='"+cls+"'>"+s+suffix+"</span>";
+};
+
+/**
+ * Converts a date to a string according to specs in fmt
+ * @returns formatted string
+ * @param d date to be formatted
+ * @param fmt string specifying the output format, may be one of the following:<dl>
+ * <dt>locale or localeDateTime</dt>
+ *   <dd>use javascript's built-in toLocaleString() function</dd>
+ * <dt>localeDate or 'Long Date'</dt>
+ *   <dd>use javascript's built-in toLocaleDateString() function</dd>
+ * <dt>translate or translateDateTime</dt>
+ *   <dd>use the formats specified in the Rico.dateFmt and Rico.timeFmt properties</dd>
+ * <dt>translateDate or 'Short Date'</dt>
+ *   <dd>use the date format specified in the Rico.dateFmt property</dd>
+ * <dt>Otherwise</dt>
+ *   <dd>Any combination of: yyyy, yy, mmmm, mmm, mm, m, dddd, ddd, dd, d, hh, h, HH, H, nn, ss, a/p</dd>
+ *</dl>
+ */
+Rico.formatDate=function(d,fmt) {
+  var datefmt=(typeof fmt=='string') ? fmt : 'translateDate';
+  switch (datefmt) {
+    case 'locale':
+    case 'localeDateTime':
+      return d.toLocaleString();
+    case 'Long Date':
+    case 'localeDate':
+      return d.toLocaleDateString();
+    case 'translate':
+    case 'translateDateTime':
+      datefmt=this.dateFmt+' '+this.timeFmt;
+      break;
+    case 'Short Date':
+    case 'translateDate':
+      datefmt=this.dateFmt;
+      break;
+  }
+  return datefmt.replace(/(yyyy|yy|mmmm|mmm|mm|dddd|ddd|dd|d|hh|nn|ss|a\/p)/gi,
+    function($1) {
+      var h;
+      switch ($1) {
+      case 'yyyy': return d.getFullYear();
+      case 'yy':   return d.getFullYear().toString().substr(2);
+      case 'mmmm': return Rico.monthNames[d.getMonth()];
+      case 'mmm':  return Rico.monthAbbr(d.getMonth());
+      case 'mm':   return Rico.zFill(d.getMonth() + 1, 2);
+      case 'm':    return (d.getMonth() + 1);
+      case 'dddd': return Rico.dayNames[d.getDay()];
+      case 'ddd':  return Rico.dayAbbr(d.getDay());
+      case 'dd':   return Rico.zFill(d.getDate(), 2);
+      case 'd':    return d.getDate();
+      case 'hh':   return Rico.zFill((h = d.getHours() % 12) ? h : 12, 2);
+      case 'h':    return ((h = d.getHours() % 12) ? h : 12);
+      case 'HH':   return Rico.zFill(d.getHours(), 2);
+      case 'H':    return d.getHours();
+      case 'nn':   return Rico.zFill(d.getMinutes(), 2);
+      case 'ss':   return Rico.zFill(d.getSeconds(), 2);
+      case 'a/p':  return d.getHours() < 12 ? 'a' : 'p';
+      }
+    }
+  );
+};
+
+/**
+ * Converts a string in ISO 8601 format to a date object.
+ * @returns date object, or false if string is not a valid date or date-time.
+ * @param string value to be converted
+ * @param offset can be used to bias the conversion and must be in minutes if provided
+ * @see Based on <a href='http://delete.me.uk/2005/03/iso8601.html'>delete.me.uk article</a>
+ */
+Rico.setISO8601=function (string,offset) {
+  if (!string) return false;
+  var d = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|(?:([-+])(\d\d)(?::?(\d\d))?)?)?)?)?)?/);
+  if (!d) return false;
+  if (!offset) offset=0;
+  var date = new Date(d[1], 0, 1);
+
+  if (d[2]) { date.setMonth(d[2] - 1); }
+  if (d[3]) { date.setDate(d[3]); }
+  if (d[4]) { date.setHours(d[4]); }
+  if (d[5]) { date.setMinutes(d[5]); }
+  if (d[6]) { date.setSeconds(d[6]); }
+  if (d[7]) { date.setMilliseconds(Number("0." + d[7]) * 1000); }
+  if (d[8]) {
+      if (d[10] && d[11]) {
+        offset = (Number(d[10]) * 60) + Number(d[11]);
+      }
+      offset *= ((d[9] == '-') ? 1 : -1);
+      offset -= date.getTimezoneOffset();
+  }
+  var time = (Number(date) + (offset * 60 * 1000));
+  date.setTime(Number(time));
+  return date;
+};
+
+/**
+ * Convert date to an ISO 8601 formatted string.
+ * @param date date object to be converted
+ * @param format an integer in the range 1-6 (default is 6):<dl>
+ * <dt>1 (year)</dt>
+ *   <dd>YYYY (eg 1997)</dd>
+ * <dt>2 (year and month)</dt>
+ *   <dd>YYYY-MM (eg 1997-07)</dd>
+ * <dt>3 (complete date)</dt>
+ *   <dd>YYYY-MM-DD (eg 1997-07-16)</dd>
+ * <dt>4 (complete date plus hours and minutes)</dt>
+ *   <dd>YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)</dd>
+ * <dt>5 (complete date plus hours, minutes and seconds)</dt>
+ *   <dd>YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)</dd>
+ * <dt>6 (complete date plus hours, minutes, seconds and a decimal
+ *   fraction of a second)</dt>
+ *   <dd>YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)</dd>
+ *</dl>
+ * @see Based on: <a href='http://www.codeproject.com/jscript/dateformat.asp'>codeproject.com article</a>
+ */
+Rico.toISO8601String=function (date, format, offset) {
+  if (!format) format=6;
+  if (!offset) {
+      offset = 'Z';
+  } else {
+      var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
+      var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
+      offsetnum *= ((d[1] == '-') ? -1 : 1);
+      date = new Date(Number(Number(date) + (offsetnum * 60000)));
+  }
+
+  var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; };
+
+  var str = date.getUTCFullYear();
+  if (format > 1) { str += "-" + zeropad(date.getUTCMonth() + 1); }
+  if (format > 2) { str += "-" + zeropad(date.getUTCDate()); }
+  if (format > 3) {
+      str += "T" + zeropad(date.getUTCHours()) +
+             ":" + zeropad(date.getUTCMinutes());
+  }
+  if (format > 5) {
+    var secs = Number(date.getUTCSeconds() + "." +
+               ((date.getUTCMilliseconds() < 100) ? '0' : '') +
+               zeropad(date.getUTCMilliseconds()));
+    str += ":" + zeropad(secs);
+  } else if (format > 4) {
+    str += ":" + zeropad(date.getUTCSeconds());
+  }
+
+  if (format > 3) { str += offset; }
+  return str;
+};
+
+/**
+ * Returns a new XML document object
+ */
+Rico.createXmlDocument=function() {
+  if (document.implementation && document.implementation.createDocument) {
+    var doc = document.implementation.createDocument("", "", null);
+    // some older versions of Moz did not support the readyState property
+    // and the onreadystate event so we patch it!
+    if (doc.readyState == null) {
+      doc.readyState = 1;
+      doc.addEventListener("load", function () {
+        doc.readyState = 4;
+        if (typeof doc.onreadystatechange == "function") {
+          doc.onreadystatechange();
+        }
+      }, false);
+    }
+    return doc;
+  }
+
+  if (window.ActiveXObject)
+      return Rico.tryFunctions(
+        function() { return new ActiveXObject('MSXML2.DomDocument');   },
+        function() { return new ActiveXObject('Microsoft.DomDocument');},
+        function() { return new ActiveXObject('MSXML.DomDocument');    },
+        function() { return new ActiveXObject('MSXML3.DomDocument');   }
+      ) || false;
+  return null;
+};
+
+/**
+ * Update the contents of an HTML element via an AJAX call
+ */
+Rico.ajaxUpdater = function(elem,url,options) {
+  this.updateSend(elem,url,options);
+};
+
+Rico.ajaxUpdater.prototype = {
+  updateSend : function(elem,url,options) {
+    this.element=elem;
+    this.onComplete=options.onComplete;
+    var self=this;
+    options.onComplete=function(xhr) { self.updateComplete(xhr); };
+    new Rico.ajaxRequest(url,options);
+  },
+
+  updateComplete : function(xhr) {
+    this.element.innerHTML=xhr.responseText;
+    if (this.onComplete) this.onComplete(xhr);
+  }
+};
+
+try {  // fix IE background image flicker (credit: www.mister-pixel.com)
+  document.execCommand("BackgroundImageCache", false, true);
+} catch(err) {}
+Rico.eventBind(window,"load", Rico.eventHandle(Rico,'windowLoaded'));
diff --git a/lib/rico3/minsrc/ricoCalendar.js b/lib/rico3/minsrc/ricoCalendar.js
new file mode 100644 (file)
index 0000000..8a30143
--- /dev/null
@@ -0,0 +1,576 @@
+/*
+ *  (c) 2005-2011 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+//  Inspired by code originally written by Tan Ling Wee on 2 Dec 2001
+
+Rico.CalendarControl = function(id,options) {
+  this.initialize(id,options);
+};
+
+Rico.CalendarControl.prototype = {
+/**
+ * @class Implements a pop-up Gregorian calendar.
+ * Dates of adoption of the Gregorian calendar vary by country - accurate as a US & British calendar from 14 Sept 1752 to present.
+ * Mark special dates with calls to addHoliday()
+ * @extends Rico.Popup
+ * @constructs
+ * @param id unique identifier
+ * @param options object may contain any of the following:<dl>
+ *   <dt>startAt       </dt><dd> week starts with 0=sunday, 1=monday? default=0</dd>
+ *   <dt>showWeekNumber</dt><dd> show week number in first column? default=0</dd>
+ *   <dt>showToday     </dt><dd> show "Today is..." in footer? default=1</dd>
+ *   <dt>dateFmt       </dt><dd> date format for return value (one of values accepted by {@link Date#formatDate}), default=ISO8601</dd>
+ *   <dt>minDate       </dt><dd> earliest selectable date? default=today-50 years</dd>
+ *   <dt>maxDate       </dt><dd> last selectable date? default=today+50 years</dd>
+ *</dl>
+ */
+  initialize: function(id,options) {
+    this.id=id;
+    var today=new Date();
+    this.defaultMin = new Date(today.getFullYear()-50,0,1);
+    this.defaultMax = new Date(today.getFullYear()+50,11,31);
+    Rico.extend(this, new Rico.Popup());
+    Rico.extend(this.options, {
+      ignoreClicks:true,
+      startAt : 0,
+      showWeekNumber : 0,
+      showToday : 1,
+      dateFmt : 'ISO8601',
+      minDate : this.defaultMin,
+      maxDate : this.defaultMax
+    });
+    Rico.extend(this.options, options || {});
+    this.bPageLoaded=false;
+    this.Holidays={};
+    this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
+    this.setDateFmt(this.options.dateFmt);
+    var self=this;
+    Rico.onLoad(function() { self.atLoad(); })
+  },
+
+
+  setDateFmt: function(fmt) {
+    this.dateFmt=(fmt=='rico') ? Rico.dateFmt : fmt;
+    Rico.log(this.id+' date format set to '+this.dateFmt);
+    this.dateParts={};
+    if (this.re.exec(this.dateFmt)) {
+      this.dateParts[RegExp.$1]=0;
+      this.dateParts[RegExp.$3]=1;
+      this.dateParts[RegExp.$5]=2;
+    }
+  },
+  
+/**
+ * Call before displaying calendar to highlight special days
+ * @param d day (1-31)
+ * @param m month (1-12)
+ * @param y year (0 implies a repeating holiday)
+ * @param desc description
+ * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
+ * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
+ */
+  addHoliday : function(d, m, y, desc, bgColor, txtColor) {
+    this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
+  },
+  
+/** @private */
+  holidayKey : function(y,m,d) {
+    return 'h'+Rico.zFill(y,4)+Rico.zFill(m,2)+Rico.zFill(d,2);
+  },
+
+  atLoad : function() {
+    Rico.log('Calendar#atLoad: '+this.id);
+    var div=Rico.$(this.id);
+    if (div) {
+      this.setDiv(div);
+    } else {
+      this.createContainer();
+      this.container.id=this.id;
+    }
+    Rico.addClass(this.content, Rico.theme.calendar || 'ricoCalContainer');
+    this.direction=Rico.direction(this.container);
+
+    var r,c,i,j,dow,a,s,tab;
+    this.colStart=this.options.showWeekNumber ? 1 : 0;
+    var colcnt=7+this.colStart
+    this.maintab=document.createElement("table");
+    this.maintab.cellSpacing=2;
+    this.maintab.cellPadding=0;
+    this.maintab.border=0;
+    this.maintab.style.borderCollapse='separate';
+    this.maintab.className=Rico.theme.calendarTable || 'ricoCalTab';
+
+    // thead (Navigation controls)
+    this.thead=this.maintab.createTHead();
+    r=this.thead.insertRow(-1);
+    this.heading=r.insertCell(-1);
+    this.heading.colSpan=colcnt;
+    //this.heading=this.content.appendChild(document.createElement("div"));
+    this.heading.className='RicoCalHeading';
+    if (Rico.theme.calendarHeading) Rico.addClass(this.heading,Rico.theme.calendarHeading)
+
+    // table footer (today)
+    if (this.options.showToday) {
+      this.tfoot=this.maintab.createTFoot();
+      this.tfoot.className='ricoCalFoot';
+      r=this.tfoot.insertRow(-1);
+      this.todayCell=r.insertCell(-1);
+      this.todayCell.colSpan=colcnt;
+      this.todayCell.className=Rico.theme.calendarFooter || 'ricoCalFoot';
+      Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
+    }
+
+    this.tbody=Rico.getTBody(this.maintab);
+    this.tbody.className='ricoCalBody';
+
+    this.content.style.display='block';
+    if (this.position == 'absolute') {
+      this.content.style.width='auto';
+      this.maintab.style.width='auto';
+    } else {
+      this.container.style.position='relative';
+      this.heading.style.position='static';  // fixes issue with ie7
+      this.content.style.padding='0px';
+      this.content.style.width='15em';
+      this.maintab.style.width='100%';
+    }
+
+    this.styles=[];
+    for (i=0; i<7; i++) {
+      r=this.tbody.insertRow(-1);
+      r.className=i==0 ? 'ricoCalDayNames' : 'row'+i;
+      if (this.options.showWeekNumber) {
+        c=r.insertCell(-1);
+        c.className='ricoCalWeekNum';
+        if (i==0) c.innerHTML=Rico.getPhraseById("calWeekHdg");
+      }
+      for (j=0; j<7; j++) {
+        c=r.insertCell(-1);
+        if (i==0) {
+          dow=(j+this.options.startAt) % 7;
+          c.innerHTML=Rico.dayAbbr(dow);
+          this.styles[j]='ricoCal'+dow;
+        } else {
+          c.className=this.styles[j];
+          if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
+        }
+      }
+    }
+    
+    this.content.appendChild(this.maintab);
+    new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
+
+    this.navtab=this.heading.appendChild(document.createElement("table"));
+    this.navrow=this.navtab.insertRow(-1);
+    this._createTitleSection('Month');
+    this.navrow.insertCell(-1).innerHTML="&nbsp;&nbsp;";
+    this._createTitleSection('Year');
+    new Rico.HoverSet(this.heading.getElementsByTagName('a'));
+    if (this.position == 'absolute') this.heading.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
+    
+    // month selector
+    this.monthPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
+    this.monthPopup.content.className='ricoCalMonthPrompt';
+    tab=document.createElement("table");
+    tab.className='ricoCalMenu';
+    if (Rico.theme.calendarPopdown) Rico.addClass(tab,Rico.theme.calendarPopdown);
+    tab.cellPadding=2;
+    tab.cellSpacing=0;
+    tab.border=0;
+    tab.style.borderCollapse='separate';
+    tab.style.margin='0px';
+    for (i=0; i<4; i++) {
+      r=tab.insertRow(-1);
+      for (j=0; j<3; j++) {
+        c=r.insertCell(-1);
+        a=document.createElement("a");
+        a.innerHTML=Rico.monthAbbr(i*3+j);
+        a.name=i*3+j;
+        if (Rico.theme.calendarDay) Rico.addClass(a,Rico.theme.calendarDay);
+        c.appendChild(a);
+        Rico.eventBind(a,"click", Rico.eventHandle(this,'selectMonth'), false);
+      }
+    }
+    new Rico.HoverSet(tab.getElementsByTagName('a'));
+    this.monthPopup.content.appendChild(tab);
+    this.container.appendChild(this.monthPopup.container);
+    this.monthPopup.closePopup();
+    
+    // year selector
+    this.yearPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
+    this.yearPopup.content.className='ricoCalYearPrompt';
+    if (Rico.theme.calendarPopdown) Rico.addClass(this.yearPopup.content,Rico.theme.calendarPopdown);
+    this.yearPrompt=document.createElement("p");
+    this.yearPrompt.innerHTML="&nbsp;";
+    var p2=document.createElement("p");
+    this.yearInput=p2.appendChild(document.createElement("input"));
+    this.yearInput.maxlength=4;
+    this.yearInput.size=4;
+    Rico.eventBind(this.yearInput,"keyup", Rico.eventHandle(this,'yearKey'), false);
+    a=Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear'));
+    p2.appendChild(a);
+    a=Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear'));
+    p2.appendChild(a);
+    this.yearPopup.content.appendChild(this.yearPrompt);
+    this.yearPopup.content.appendChild(p2);
+    this.container.appendChild(this.yearPopup.container);
+    this.yearPopup.closePopup();
+
+    // fix anchors so they work in IE6
+    a=this.content.getElementsByTagName('a');
+    for (i=0; i<a.length; i++) {
+      a[i].href='javascript:void(0)';
+    }
+    
+    Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
+    this.bPageLoaded=true;
+  },
+
+  _createTitleSection : function(section) {
+    var arrows=['left','right'];
+    if (this.direction=='rtl') arrows.reverse();
+    var c=this.navrow.insertCell(-1);
+    var a=c.appendChild(document.createElement("a"));
+    a.className='Rico_'+arrows[0]+'Arrow';
+    a.appendChild(this._createNavArrow(arrows[0]));
+    Rico.eventBind(a,"click", Rico.eventHandle(this,'dec'+section), false);
+
+    c=this.navrow.insertCell(-1);
+    a=c.appendChild(document.createElement("a"));
+    Rico.eventBind(a,"click", Rico.eventHandle(this,'popUp'+section), false);
+    this['title'+section]=a;
+
+    c=this.navrow.insertCell(-1);
+    a=c.appendChild(document.createElement("a"));
+    a.className='Rico_'+arrows[1]+'Arrow';
+    a.appendChild(this._createNavArrow(arrows[1]));
+    Rico.eventBind(a,"click", Rico.eventHandle(this,'inc'+section), false);
+  },
+  
+  _createNavArrow: function(direction) {
+    var span=document.createElement("span");
+    span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
+    span.style.display="inline-block";
+    return span;
+  },
+
+  selectNow : function() {
+    var today = new Date();
+    this.dateNow  = today.getDate();
+    this.monthNow = today.getMonth();
+    this.yearNow  = today.getFullYear();
+    this.monthSelected=this.monthNow;
+    this.yearSelected=this.yearNow;
+    this.constructCalendar();
+  },
+  
+/**
+ * @returns true if yr/mo is within minDate/MaxDate
+ */
+  isValidMonth : function(yr,mo) {
+    if (yr < this.options.minDate.getFullYear()) return false;
+    if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
+    if (yr > this.options.maxDate.getFullYear()) return false;
+    if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
+    return true;
+  },
+
+  incMonth : function(e) {
+    if (e) Rico.eventStop(e);
+    var newMonth=this.monthSelected+1;
+    var newYear=this.yearSelected;
+    if (newMonth>11) {
+      newMonth=0;
+      newYear++;
+    }
+    if (!this.isValidMonth(newYear,newMonth)) return;
+    this.monthSelected=newMonth;
+    this.yearSelected=newYear;
+    this.constructCalendar();
+  },
+
+  decMonth : function(e) {
+    if (e) Rico.eventStop(e);
+    var newMonth=this.monthSelected-1;
+    var newYear=this.yearSelected;
+    if (newMonth<0) {
+      newMonth=11;
+      newYear--;
+    }
+    if (!this.isValidMonth(newYear,newMonth)) return;
+    this.monthSelected=newMonth;
+    this.yearSelected=newYear;
+    this.constructCalendar();
+  },
+  
+/** @private */
+  selectMonth : function(e) {
+    var el=Rico.eventElement(e);
+    this.monthSelected=parseInt(el.name,10);
+    this.constructCalendar();
+    Rico.eventStop(e);
+  },
+  
+  // position: 0=left, 1=right
+  openYrMo : function(popup,position) {
+    if (this.direction=='rtl') position=1-position;
+    popup.openPopup();
+    var left=position ? this.content.offsetWidth - popup.container.offsetWidth - 5 : 3;
+    popup.move(left, this.heading.offsetHeight+2);
+  },
+
+  popUpMonth : function(e) {
+    Rico.eventStop(e);
+    if (this.monthPopup.visible()) {
+      this.popDownMonth();
+      return false;
+    }
+    this.popDownYear();
+    this.openYrMo(this.monthPopup,0);
+    return false;
+  },
+
+  popDownMonth : function() {
+    this.monthPopup.closePopup();
+  },
+
+  popDownYear : function() {
+    this.yearPopup.closePopup();
+    this.yearInput.disabled=true;  // make sure this does not get submitted
+  },
+
+/**
+ * Prompt for year
+ */
+  popUpYear : function(e) {
+    Rico.eventStop(e);
+    if (this.yearPopup.visible()) {
+      this.popDownYear();
+      return false;
+    }
+    this.popDownMonth();
+    this.yearPrompt.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
+    this.yearInput.disabled=false;
+    this.yearInput.value='';   // this.yearSelected
+    this.openYrMo(this.yearPopup,1);
+    var self=this;
+    setTimeout(function() { self.yearInput.focus(); }, 10);  // ie8 has issues without this delay
+    return false;
+  },
+  
+  yearKey : function(e) {
+    switch (Rico.eventKey(e)) {
+      case 27: this.popDownYear(); Rico.eventStop(e); return false;
+      case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
+    }
+    return true;
+  },
+  
+  processPopUpYear : function() {
+    var newYear=this.yearInput.value;
+    newYear=parseInt(newYear,10);
+    if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
+      alert(Rico.getPhraseById("calInvalidYear"));
+    } else {
+      this.yearSelected=newYear;
+      this.popDownYear();
+      this.constructCalendar();
+    }
+  },
+  
+  incYear : function(e) {
+    if (e) Rico.eventStop(e);
+    if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
+    this.yearSelected++;
+    this.constructCalendar();
+  },
+
+  decYear : function(e) {
+    if (e) Rico.eventStop(e);
+    if (this.yearSelected<=this.options.minDate.getFullYear()) return;
+    this.yearSelected--;
+    this.constructCalendar();
+  },
+
+  // tried a number of different week number functions posted on the net
+  // this is the only one that produced consistent results when comparing week numbers for December and the following January
+  WeekNbr : function(year,month,day) {
+    var when = new Date(year,month,day);
+    var newYear = new Date(year,0,1);
+    var offset = 7 + 1 - newYear.getDay();
+    if (offset == 8) offset = 1;
+    var daynum = ((Date.UTC(year,when.getMonth(),when.getDate(),0,0,0) - Date.UTC(year,0,1,0,0,0)) /1000/60/60/24) + 1;
+    var weeknum = Math.floor((daynum-offset+7)/7);
+    if (weeknum == 0) {
+      year--;
+      var prevNewYear = new Date(year,0,1);
+      var prevOffset = 7 + 1 - prevNewYear.getDay();
+      weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
+    }
+    return weeknum;
+  },
+
+  constructCalendar : function() {
+    var aNumDays = [31,0,31,30,31,30,31,31,30,31,30,31];
+    var startDate = new Date (this.yearSelected,this.monthSelected,1);
+    var endDate,numDaysInMonth,i,colnum;
+
+    if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
+      alert('ERROR in calendar: monthSelected='+this.monthSelected);
+      return;
+    }
+
+    if (this.monthSelected==1) {
+      endDate = new Date (this.yearSelected,this.monthSelected+1,1);
+      endDate = new Date (endDate - (24*60*60*1000));
+      numDaysInMonth = endDate.getDate();
+    } else {
+      numDaysInMonth = aNumDays[this.monthSelected];
+    }
+    var dayPointer = startDate.getDay() - this.options.startAt;
+    if (dayPointer<0) dayPointer+=7;
+    this.popDownMonth();
+    this.popDownYear();
+
+    //this.bgcolor=Rico.getStyle(this.tbody,'background-color');
+    //this.bgcolor=this.bgcolor.replace(/\"/g,'');
+    if (this.options.showWeekNumber) {
+      for (i=1; i<7; i++) {
+        this.tbody.rows[i].cells[0].innerHTML='&nbsp;';
+      }
+    }
+    for ( i=0; i<dayPointer; i++ ) {
+      this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
+    }
+
+    for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
+      colnum=dayPointer % 7;
+      if (this.options.showWeekNumber && colnum==0) {
+        this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
+      }
+      var c=this.tbody.rows[r].cells[colnum+this.colStart];
+      c.innerHTML=datePointer;
+      c.className=this.styles[colnum];
+      if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
+        Rico.addClass(c,Rico.theme.calendarToday || 'ricoCalToday');
+      }
+      if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
+      if ((datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected)) {
+        Rico.addClass(c,Rico.theme.calendarSelectedDay || 'ricoSelectedDay');
+      }
+      var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
+      if (!h)  {
+        h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
+      }
+      c.style.color=h ? h.txtColor : '';
+      c.style.backgroundColor=h ? h.bgColor : '';
+      c.title=h ? h.desc : '';
+      c.style.visibility='visible';
+      if (colnum==6) r++;
+    }
+    while (dayPointer<42) {
+      colnum=dayPointer % 7;
+      this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
+      dayPointer++;
+      if (colnum==6) r++;
+    }
+
+    this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
+    this.titleYear.innerHTML = this.yearSelected;
+    if (this.todayCell) {
+      this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
+    }
+  },
+  
+/** @private */
+  resetCell: function(c) {
+    c.innerHTML="&nbsp;";
+    c.title='';
+    c.style.visibility='hidden';
+  },
+  
+  close: function(e) {
+    if (e) Rico.eventStop(e);
+    this.closePopup();
+  },
+  
+/** @private */
+  saveAndClose : function(e) {
+    Rico.eventStop(e);
+    var el=Rico.eventElement(e);
+    var s=el.innerHTML.replace(/&nbsp;/g,'');
+    if (s=='' || el.className=='ricoCalWeekNum') return;
+    var day=parseInt(s,10);
+    if (isNaN(day)) return;
+    var d=new Date(this.yearSelected,this.monthSelected,day);
+    var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
+    if (this.returnValue) {
+      this.returnValue(dateStr);
+      this.closePopup();
+    }
+  },
+
+  open : function(curval,column) {
+    if (!this.bPageLoaded) return;
+    if (column) {
+      this.setDateFmt(column.format.dateFmt);
+      this.options.minDate=column.format.min || this.defaultMin;
+      this.options.maxDate=column.format.max || this.defaultMax;
+    }
+    var today = new Date();
+    this.dateNow  = today.getDate();
+    this.monthNow = today.getMonth();
+    this.yearNow  = today.getFullYear();
+    this.oyearSelected = -1;
+    if (typeof curval=='object') {
+      this.odateSelected  = curval.getDate();
+      this.omonthSelected = curval.getMonth();
+      this.oyearSelected  = curval.getFullYear();
+    } else if (this.dateFmt=='ISO8601') {
+      var d=Rico.setISO8601(curval);
+      if (d) {
+        this.odateSelected  = d.getDate();
+        this.omonthSelected = d.getMonth();
+        this.oyearSelected  = d.getFullYear();
+      }
+    } else if (this.re.exec(curval)) {
+      var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
+      this.odateSelected  = parseInt(aDate[this.dateParts.dd], 10);
+      this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
+      this.oyearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
+      if (this.oyearSelected < 100) {
+        // apply a century to 2-digit years
+        this.oyearSelected+=this.yearNow - (this.yearNow % 100);
+        var maxyr=this.options.maxDate.getFullYear();
+        while (this.oyearSelected > maxyr) this.oyearSelected-=100;
+      }
+    } else {
+      if (curval) {
+        alert('ERROR: invalid date passed to calendar ('+curval+')');
+      }
+    }
+    if (this.oyearSelected > 0) {
+      this.dateSelected=this.odateSelected;
+      this.monthSelected=this.omonthSelected;
+      this.yearSelected=this.oyearSelected;
+    } else {
+      this.dateSelected=this.dateNow;
+      this.monthSelected=this.monthNow;
+      this.yearSelected=this.yearNow;
+    }
+    this.constructCalendar();
+    this.openPopup();
+  }
+};
diff --git a/lib/rico3/minsrc/ricoGridCommon.js b/lib/rico3/minsrc/ricoGridCommon.js
new file mode 100644 (file)
index 0000000..e759134
--- /dev/null
@@ -0,0 +1,953 @@
+/*
+ *  (c) 2005-2009 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+if(typeof Rico=='undefined') throw("GridCommon requires the Rico JavaScript framework");
+
+/**
+ * Define methods that are common to both SimpleGrid and LiveGrid
+ */
+Rico.GridCommon = {
+
+  baseInit: function() {
+    this.options = {
+      saveColumnInfo   : {width:true, filter:false, sort:false},  // save info in cookies?
+      cookiePrefix     : 'RicoGrid.',
+      allowColResize   : true,      // allow user to resize columns
+      windowResize     : true,      // Resize grid on window.resize event? Set to false when embedded in an accordian.
+      click            : null,
+      dblclick         : null,
+      contextmenu      : null,
+      menuEvent        : null,      // event that triggers menus - click, dblclick, contextmenu, or none (no menus)
+      defaultWidth     : -1,        // if -1, then use unformatted column width
+      scrollBarWidth   : 19,        // this is the value used in positioning calculations, it does not actually change the width of the scrollbar
+      minScrollWidth   : 100,       // min scroll area width when width of frozen columns exceeds window width
+      frozenColumns    : 0,
+      exportWindow     : "height=400,width=500,scrollbars=1,menubar=1,resizable=1,location=0,toolbar=0,status=0",
+      exportStyleList  : ['background-color','color','text-align','font-weight','font-size','font-family'],
+      exportImgTags    : false,       // applies to grid header and to SimpleGrid cells (not LiveGrid cells)
+      exportFormFields : true,
+      FilterLocation   : null,        // heading row number to place filters. -1=add a new heading row.
+      FilterAllToken   : '___ALL___', // select box value to use to indicate ALL
+      columnSpecs      : []
+    };
+    this.hdrCells=[];
+    this.headerColCnt=0;
+    this.headerRowIdx=0;       // row in header which gets resizers (no colspan's in this row)
+    this.tabs=new Array(2);
+    this.thead=new Array(2);
+    this.tbody=new Array(2);
+  },
+
+  attachMenuEvents: function() {
+    var i;
+    if (!this.options.menuEvent || this.options.menuEvent=='none') return;
+    this.hideScroll=navigator.userAgent.match(/Macintosh\b.*\b(Firefox|Camino)\b/i) || (Rico.isOpera && parseFloat(window.opera.version())<9.5);
+    this.options[this.options.menuEvent]=Rico.eventHandle(this,'handleMenuClick');
+    if (this.highlightDiv) {
+      switch (this.options.highlightElem) {
+        case 'cursorRow':
+          this.attachMenu(this.highlightDiv[0]);
+          break;
+        case 'cursorCell':
+          for (i=0; i<2; i++) {
+            this.attachMenu(this.highlightDiv[i]);
+          }
+          break;
+      }
+    }
+    for (i=0; i<2; i++) {
+      this.attachMenu(this.tbody[i]);
+    }
+  },
+
+  attachMenu: function(elem) {
+    if (this.options.click)
+      Rico.eventBind(elem, 'click', this.options.click, false);
+    if (this.options.dblclick) {
+      if (Rico.isWebKit || Rico.isOpera)
+        Rico.eventBind(elem, 'click', Rico.eventHandle(this,'handleDblClick'), false);
+      else
+        Rico.eventBind(elem, 'dblclick', this.options.dblclick, false);
+    }
+    if (this.options.contextmenu) {
+      if (Rico.isOpera || Rico.isKonqueror)
+        Rico.eventBind(elem, 'click', Rico.eventHandle(this,'handleContextMenu'), false);
+      else
+        Rico.eventBind(elem, 'contextmenu', this.options.contextmenu, false);
+    }
+  },
+
+/**
+ * implement double-click for browsers that don't support a double-click event (e.g. Safari)
+ */
+  handleDblClick: function(e) {
+    var elem=Rico.eventElement(e);
+    if (this.dblClickElem == elem) {
+      this.options.dblclick(e);
+    } else {
+      this.dblClickElem = elem;
+      this.safariTimer=Rico.runLater(300,this,'clearDblClick');
+    }
+  },
+
+  clearDblClick: function() {
+    this.dblClickElem=null;
+  },
+
+/**
+ * implement right-click for browsers that don't support contextmenu event (e.g. Opera, Konqueror)
+ * use control-click instead
+ */
+  handleContextMenu: function(e) {
+    var b;
+    if( typeof( e.which ) == 'number' )
+      b = e.which; //Netscape compatible
+    else if( typeof( e.button ) == 'number' )
+      b = e.button; //DOM
+    else
+      return;
+    if (b==1 && e.ctrlKey) {
+      this.options.contextmenu(e);
+    }
+  },
+
+  cancelMenu: function() {
+    if (this.menu) this.menu.cancelmenu();
+  },
+
+/**
+ * gather info from original headings
+ */
+  getColumnInfo: function(hdrSrc) {
+    Rico.log('getColumnInfo: len='+hdrSrc.length);
+    if (hdrSrc.length == 0) return 0;
+    this.headerRowCnt=hdrSrc.length;
+    var r,c,colcnt;
+    for (r=0; r<this.headerRowCnt; r++) {
+      var headerRow = hdrSrc[r];
+      var headerCells=headerRow.cells;
+      if (r >= this.hdrCells.length) this.hdrCells[r]=[];
+      for (c=0; c<headerCells.length; c++) {
+        var obj={};
+        obj.cell=headerCells[c];
+        obj.colSpan=headerCells[c].colSpan || 1;  // Safari & Konqueror return default colspan of 0
+        if (this.options.defaultWidth < 0) obj.initWidth=headerCells[c].offsetWidth;
+        this.hdrCells[r].push(obj);
+      }
+      if (headerRow.id.slice(-5)=='_main') {
+        colcnt=this.hdrCells[r].length;
+        this.headerRowIdx=r;
+      }
+    }
+    if (!colcnt) {
+      this.headerRowIdx=this.headerRowCnt-1;
+      colcnt=this.hdrCells[this.headerRowIdx].length;
+    }
+    Rico.log("getColumnInfo: colcnt="+colcnt);
+    return colcnt;
+  },
+
+  addHeadingRow: function(className) {
+    var r=this.headerRowCnt++;
+    this.hdrCells[r]=[];
+    for( var h=0; h < 2; h++ ) {
+      var row = this.thead[h].insertRow(-1);
+      var newClass='ricoLG_hdg '+this.tableId+'_hdg'+r;
+      if (className) newClass+=' '+className;
+      row.className=newClass;
+      var limit= h==0 ? this.options.frozenColumns : this.headerColCnt-this.options.frozenColumns;
+      for( var c=0; c < limit; c++ ) {
+        var hdrCell=row.insertCell(-1);
+        var colDiv=Rico.wrapChildren(hdrCell,'ricoLG_col');
+        Rico.wrapChildren(colDiv,'ricoLG_cell');
+        this.hdrCells[r].push({cell:hdrCell,colSpan:1});
+      }
+    }
+    return r;
+  },
+  
+/**
+ * create column array
+ */
+  createColumnArray: function(columnType) {
+    this.direction=Rico.getStyle(this.outerDiv,'direction').toLowerCase();  // ltr or rtl
+    this.align=this.direction=='rtl' ? ['right','left'] : ['left','right'];
+    Rico.log('createColumnArray: dir='+this.direction);
+    for (var i=0; i<2; i++) Rico.addClass(this.thead[i].rows[this.headerRowIdx],'rico_ResizeRow');
+    this.columns = [];
+    for (var c=0; c < this.headerColCnt; c++) {
+      Rico.log("createColumnArray: c="+c);
+      var tabidx=c<this.options.frozenColumns ? 0 : 1;
+      var col=new Rico[columnType](this, c, this.hdrCells[this.headerRowIdx][c], tabidx);
+      this.columns.push(col);
+      if (c > 0) this.columns[c-1].next=col;
+    }
+    this.getCookie();
+    Rico.runLater(100,this,'insertResizers');  // avoids peek-a-boo bug in column 1 in IE6/7
+  },
+
+/**
+ * Insert resizing handles
+ */
+  insertResizers: function() {
+    if (!this.options.allowColResize) return;
+    for (var x=0;x<this.columns.length;x++) {
+      this.columns[x].insertResizer();
+    }
+  },
+
+/**
+ * Create div structure
+ */
+  createDivs: function() {
+    Rico.log("createDivs start");
+    this.outerDiv = this.createDiv("outer");
+    if (Rico.theme.gridContainer) Rico.addClass(this.outerDiv,Rico.theme.gridContainer);
+    if (this.outerDiv.firstChild && this.outerDiv.firstChild.tagName && this.outerDiv.firstChild.tagName.toUpperCase()=='TABLE') {
+      this.structTab=this.outerDiv.firstChild;
+      this.structTabLeft=this.structTab.rows[0].cells[0];
+      this.structTabUR=this.structTab.rows[0].cells[1];
+      this.structTabLR=this.structTab.rows[1].cells[0];
+    } else {
+      this.structTab = document.createElement("table");
+      this.structTab.border=0;
+      this.structTab.cellPadding=0;
+      this.structTab.cellSpacing=0;
+      var tr1=this.structTab.insertRow(-1);
+      tr1.vAlign='top';
+      this.structTabLeft=tr1.insertCell(-1);
+      this.structTabLeft.rowSpan=2;
+      this.structTabLeft.style.padding='0px';
+      this.structTabLeft.style.border='none';
+      var tr2=this.structTab.insertRow(-1);
+      tr2.vAlign='top';
+      this.structTabUR=tr1.insertCell(-1);
+      this.structTabUR.style.padding='0px';
+      this.structTabUR.style.border='none';
+      this.structTabLR=tr2.insertCell(-1);
+      this.structTabLR.style.padding='0px';
+      this.structTabLR.style.border='none';
+      this.outerDiv.appendChild(this.structTab);
+    }
+    Rico.addClass(this.structTab,'ricoLG_StructTab');
+    //this.structTabLR.style.overflow='hidden';
+    //if (Rico.isOpera) this.outerDiv.style.overflow="hidden";
+    this.frozenTabs = this.createDiv("frozenTabs",this.structTabLeft);
+    this.innerDiv   = this.createDiv("inner",this.structTabUR);
+    this.scrollDiv  = this.createDiv("scroll",this.structTabLR);
+    this.resizeDiv  = this.createDiv("resize",this.outerDiv,true);
+
+    this.messagePopup=new Rico.Popup();
+    this.messagePopup.createContainer({hideOnEscape:false, hideOnClick:false, parent:this.outerDiv});
+    this.messagePopup.content.className='ricoLG_messageDiv';
+    if (Rico.theme.gridMessage) Rico.addClass(this.messagePopup.content,Rico.theme.gridMessage);
+
+    this.keywordPopup=new Rico.Window('', {zIndex:-1, parent:this.outerDiv});
+    Rico.addClass(this.keywordPopup.container, 'ricoLG_keywordDiv');
+    var instructions=this.keywordPopup.contentDiv.appendChild(document.createElement("p"));
+    instructions.innerHTML=Rico.getPhraseById("keywordPrompt");
+    this.keywordBox=this.keywordPopup.contentDiv.appendChild(document.createElement("input"));
+    this.keywordBox.size=20;
+    Rico.eventBind(this.keywordBox,"keypress", Rico.eventHandle(this,'keywordKey'), false);
+    this.keywordPopup.contentDiv.appendChild(Rico.floatButton('Checkmark', Rico.eventHandle(this,'processKeyword')));
+    var s=this.keywordPopup.contentDiv.appendChild(document.createElement("p"));
+    Rico.setStyle(s,{clear:'both'});
+
+    //this.frozenTabs.style[this.align[0]]='0px';
+    //this.innerDiv.style[this.align[0]]='0px';
+    Rico.log("createDivs end");
+  },
+  
+  keywordKey: function(e) {
+    switch (Rico.eventKey(e)) {
+      case 27: this.closeKeyword(); Rico.eventStop(e); return false;
+      case 13: this.processKeyword(); Rico.eventStop(e); return false;
+    }
+    return true;
+  },
+  
+  openKeyword: function(colnum) {
+    this.keywordCol=colnum;
+    this.keywordBox.value='';
+    this.keywordPopup.setTitle(this.columns[colnum].displayName);
+    this.keywordPopup.centerPopup();
+    this.keywordBox.focus();
+  },
+  
+  closeKeyword: function() {
+    this.keywordPopup.closePopup();
+    this.cancelMenu();
+  },
+  
+  processKeyword: function() {
+    var keyword=this.keywordBox.value;
+    this.closeKeyword();
+    this.columns[this.keywordCol].setFilterKW(keyword);
+  },
+
+/**
+ * Create a div and give it a standardized id and class name.
+ * If the div already exists, then just assign the class name.
+ */
+  createDiv: function(elemName,elemParent,hidden) {
+    var id=this.tableId+"_"+elemName+"Div";
+    var newdiv=document.getElementById(id);
+    if (!newdiv) {
+      newdiv = document.createElement("div");
+      newdiv.id = id;
+      if (elemParent) elemParent.appendChild(newdiv);
+    }
+    newdiv.className = "ricoLG_"+elemName+"Div";
+    if (hidden) Rico.hide(newdiv);
+    return newdiv;
+  },
+
+/**
+ * Common code used to size & position divs in both SimpleGrid & LiveGrid
+ */
+  baseSizeDivs: function() {
+    this.setOtherHdrCellWidths();
+
+    if (this.options.frozenColumns) {
+      Rico.show(this.tabs[0]);
+      Rico.show(this.frozenTabs);
+      // order of next 3 lines is critical in IE6
+      this.hdrHt=Math.max(Rico.nan2zero(this.thead[0].offsetHeight),this.thead[1].offsetHeight);
+      this.dataHt=Math.max(Rico.nan2zero(this.tbody[0].offsetHeight),this.tbody[1].offsetHeight);
+      this.frzWi=this.borderWidth(this.tabs[0]);
+    } else {
+      Rico.hide(this.tabs[0]);
+      Rico.hide(this.frozenTabs);
+      this.frzWi=0;
+      this.hdrHt=this.thead[1].offsetHeight;
+      this.dataHt=this.tbody[1].offsetHeight;
+    }
+
+    var wiLimit,i;
+    var borderWi=this.borderWidth(this.columns[0].dataCell);
+    Rico.log('baseSizeDivs '+this.tableId+': hdrHt='+this.hdrHt+' dataHt='+this.dataHt);
+    Rico.log(this.tableId+' frzWi='+this.frzWi+' borderWi='+borderWi);
+    for (i=0; i<this.options.frozenColumns; i++) {
+      if (this.columns[i].visible) this.frzWi+=parseInt(this.columns[i].colWidth,10)+borderWi;
+    }
+    this.scrTabWi=this.borderWidth(this.tabs[1]);
+    this.scrTabWi0=this.scrTabWi;
+    Rico.log('scrTabWi: '+this.scrTabWi);
+    for (i=this.options.frozenColumns; i<this.columns.length; i++) {
+      if (this.columns[i].visible) this.scrTabWi+=parseInt(this.columns[i].colWidth,10)+borderWi;
+    }
+    this.scrWi=this.scrTabWi+this.options.scrollBarWidth;
+    if (this.sizeTo=='parent') {
+      if (Rico.isIE) Rico.hide(this.outerDiv);
+      wiLimit=this.outerDiv.parentNode.offsetWidth;
+      if (Rico.isIE) Rico.show(this.outerDiv);
+    }  else {
+     if (!Rico.isWebKit)
+      wiLimit=Rico.windowWidth()-this.options.scrollBarWidth-8;
+    }
+    if (this.outerDiv.parentNode.clientWidth > 0)
+      wiLimit=Math.min(this.outerDiv.parentNode.clientWidth, wiLimit);
+    var overage=this.frzWi+this.scrWi-wiLimit;
+    Rico.log('baseSizeDivs '+this.tableId+': scrWi='+this.scrWi+' wiLimit='+wiLimit+' overage='+overage+' clientWidth='+this.outerDiv.parentNode.clientWidth);
+    if (overage > 0 && this.options.frozenColumns < this.columns.length)
+      this.scrWi=Math.max(this.scrWi-overage, this.options.minScrollWidth);
+    this.scrollDiv.style.width=this.scrWi+'px';
+    //this.scrollDiv.style.top=this.hdrHt+'px';
+    //this.frozenTabs.style.width=this.scrollDiv.style[this.align[0]]=this.innerDiv.style[this.align[0]]=this.frzWi+'px';
+    this.frozenTabs.style.width=this.frzWi+'px';
+    this.outerDiv.style.width=(this.frzWi+this.scrWi)+'px';
+  },
+
+/**
+ * Returns the sum of the left & right border widths of an element
+ */
+  borderWidth: function(elem) {
+    var l=Rico.nan2zero(Rico.getStyle(elem,'borderLeftWidth'));
+    var r=Rico.nan2zero(Rico.getStyle(elem,'borderRightWidth'));
+    Rico.log((elem.id || elem.tagName)+' borderWidth: L='+l+', R='+r);
+    return l + r;
+//    return Rico.nan2zero(Rico.getStyle(elem,'borderLeftWidth')) + Rico.nan2zero(Rico.getStyle(elem,'borderRightWidth'));
+  },
+
+  setOtherHdrCellWidths: function() {
+    var c,i,j,r,w,hdrcell,cell,origSpan,newSpan,divs;
+    for (r=0; r<this.hdrCells.length; r++) {
+      if (r==this.headerRowIdx) continue;
+      Rico.log('setOtherHdrCellWidths: r='+r);
+      c=i=0;
+      while (i<this.headerColCnt && c<this.hdrCells[r].length) {
+        hdrcell=this.hdrCells[r][c];
+        cell=hdrcell.cell;
+        origSpan=newSpan=hdrcell.colSpan;
+        for (w=j=0; j<origSpan; j++, i++) {
+          if (this.columns[i].hdrCell.style.display=='none')
+            newSpan--;
+          else if (this.columns[i].hdrColDiv.style.display!='none')
+            w+=parseInt(this.columns[i].colWidth,10);
+        }
+        if (!hdrcell.hdrColDiv || !hdrcell.hdrCellDiv) {
+          divs=cell.getElementsByTagName('div');
+          hdrcell.hdrColDiv=(divs.length<1) ? Rico.wrapChildren(cell,'ricoLG_col') : divs[0];
+          hdrcell.hdrCellDiv=(divs.length<2) ? Rico.wrapChildren(hdrcell.hdrColDiv,'ricoLG_cell') : divs[1];
+        }
+        if (newSpan==0) {
+          cell.style.display='none';
+        } else if (w==0) {
+          hdrcell.hdrColDiv.style.display='none';
+          cell.colSpan=newSpan;
+        } else {
+          cell.style.display='';
+          hdrcell.hdrColDiv.style.display='';
+          cell.colSpan=newSpan;
+          hdrcell.hdrColDiv.style.width=w+'px';
+        }
+        c++;
+      }
+    }
+  },
+
+  initFilterImage: function(filterRowNum){
+    this.filterAnchor=document.getElementById(this.tableId+'_filterLink');
+    if (!this.filterAnchor) return;
+    this.filterRows=Rico.select('tr.'+this.tableId+'_hdg'+filterRowNum);
+    if (this.filterRows.length!=2) return;
+    for (var i=0, r=[]; i<2; i++) r[i]=Rico.select('.ricoLG_cell',this.filterRows[i]);
+    this.filterElements=r[0].concat(r[1]);
+    this.saveHeight = this.filterElements[0].offsetHeight;
+    var pt=Rico.getStyle(this.filterElements[0],'paddingTop');
+    var pb=Rico.getStyle(this.filterElements[0],'paddingBottom');
+    if (pt) this.saveHeight-=parseInt(pt,10);
+    if (pb) this.saveHeight-=parseInt(pb,10);
+    this.rowNum = filterRowNum;
+    this.setFilterImage(false);
+    //Rico.eventBind(this.filterAnchor, 'click', Rico.eventHandle(this,'toggleFilterRow'), false);
+  },
+
+  toggleFilterRow: function() {
+    if ( Rico.visible(this.filterRows[0]) )
+      this.slideFilterUp();
+    else
+      this.slideFilterDown();
+  },
+
+  setFilterImage: function(expandFlag) {
+    var altText=Rico.getPhraseById((expandFlag ? 'show' : 'hide')+'FilterRow');
+    this.filterAnchor.innerHTML = '<img src="'+Rico.imgDir+'tableFilter'+(expandFlag ? 'Expand' : 'Collapse')+'.gif" alt="'+altText+'" border="0">';
+  },
+
+/**
+ * Returns a div for the cell at the specified row and column index.
+ * In SimpleGrid, r can refer to any row in the grid.
+ * In LiveGrid, r refers to a visible row (row 0 is the first visible row).
+ */
+  cell: function(r,c) {
+    return (0<=c && c<this.columns.length && r>=0) ? this.columns[c].cell(r) : null;
+  },
+
+/**
+ * Returns the screen height available for a grid
+ */
+  availHt: function() {
+    var divPos=Rico.cumulativeOffset(this.outerDiv);
+    return Rico.windowHeight()-divPos.top-2*this.options.scrollBarWidth-15;  // allow for scrollbar and some margin
+  },
+
+  hideMsg: function() {
+    this.messagePopup.closePopup();
+  },
+
+  showMsg: function(msg) {
+    this.messagePopup.setContent(msg);
+    this.messagePopup.centerPopup();
+    Rico.log("showMsg: "+msg);
+  },
+
+/**
+ * @return array of column objects which have invisible status
+ */
+  listInvisible: function(attr) {
+    var hiddenColumns=[];
+    for (var x=0;x<this.columns.length;x++) {
+      if (!this.columns[x].visible)
+        hiddenColumns.push(attr ? this.columns[x][attr] : this.columns[x]);
+    }
+    return hiddenColumns;
+  },
+
+/**
+ * @return index of left-most visibile column, or -1 if there are no visible columns
+ */
+  firstVisible: function() {
+    for (var x=0;x<this.columns.length;x++) {
+      if (this.columns[x].visible) return x;
+    }
+    return -1;
+  },
+
+/**
+ * Show all columns
+ */
+  showAll: function() {
+    var invisible=this.listInvisible();
+    for (var x=0;x<invisible.length;x++)
+      invisible[x].showColumn();
+  },
+  
+  chooseColumns: function() {
+    this.menu.cancelmenu();
+    var x,z,col,itemDiv,span,contentDiv;\r
+    if (!this.columnChooser) {
+      Rico.log('creating columnChooser');
+      z=Rico.getStyle(this.outerDiv.offsetParent,'zIndex');
+      if (typeof z!='number') z=0;
+      this.columnChooser=new Rico.Window(Rico.getPhraseById('gridChooseCols'), {zIndex:z+2, parent:this.outerDiv});
+      Rico.addClass(this.columnChooser.container, 'ricoLG_chooserDiv');
+      contentDiv=this.columnChooser.contentDiv;
+      for (x=0;x<this.columns.length;x++) {
+        col=this.columns[x];
+        itemDiv=contentDiv.appendChild(document.createElement('div'));
+        col.ChooserBox=Rico.createFormField(itemDiv,'input','checkbox');
+        span=itemDiv.appendChild(document.createElement('span'));
+        span.innerHTML=col.displayName;
+        Rico.eventBind(col.ChooserBox, 'click', Rico.eventHandle(col,'chooseColumn'), false);
+      }
+    }
+    Rico.log('opening columnChooser');
+    this.columnChooser.openPopup(3,this.hdrHt+3);
+    for (x=0;x<this.columns.length;x++) {
+      this.columns[x].ChooserBox.checked=this.columns[x].visible;
+      this.columns[x].ChooserBox.disabled = !this.columns[x].canHideShow();
+    }
+  },
+
+  blankRow: function(r) {
+    for (var c=0; c < this.columns.length; c++) {
+      this.columns[c].clearCell(r);
+    }
+  },
+  
+  getExportStyles: function(chkelem) {
+    var exportStyles=this.options.exportStyleList;
+    var bgImg=Rico.getStyle(chkelem,'backgroundImage');
+    if (!bgImg || bgImg=='none') return exportStyles;
+    for (var styles=[],i=0; i<exportStyles.length; i++)
+      if (exportStyles[i]!='background-color' && exportStyles[i]!='color') styles.push(exportStyles[i]);
+    return styles;
+  },
+
+/**
+ * Support function for printVisible()
+ */
+  exportStart: function() {
+    var r,c,i,j,hdrcell,newSpan,divs,cell;
+    var exportStyles=this.getExportStyles(this.thead[0]);
+    //alert(exportStyles.join('\n'));
+    this.exportRows=[];
+    this.exportText="<html><head></head><body><table border='1' cellspacing='0'>";\r
+    for (c=0; c<this.columns.length; c++) {\r
+      if (this.columns[c].visible) this.exportText+="<col width='"+parseInt(this.columns[c].colWidth,10)+"'>";
+    }\r
+    this.exportText+="<thead style='display: table-header-group;'>";
+    if (this.exportHeader) this.exportText+=this.exportHeader;
+    for (r=0; r<this.hdrCells.length; r++) {
+      if (this.hdrCells[r].length==0 || !Rico.visible(this.hdrCells[r][0].cell.parentNode)) continue;
+      this.exportText+="<tr>";
+      for (c=0,i=0; c<this.hdrCells[r].length; c++) {
+        hdrcell=this.hdrCells[r][c];
+        newSpan=hdrcell.colSpan;
+        for (j=0; j<hdrcell.colSpan; j++, i++) {
+          if (!this.columns[i].visible) newSpan--;
+        }
+        if (newSpan > 0) {
+          divs=Rico.select('.ricoLG_cell',hdrcell.cell);
+          cell=divs && divs.length>0 ? divs[0] : hdrcell.cell;
+          this.exportText+="<td style='"+this.exportStyle(cell,exportStyles)+"'";
+          if (hdrcell.colSpan > 1) this.exportText+=" colspan='"+newSpan+"'";
+          this.exportText+=">"+Rico.getInnerText(cell,!this.options.exportImgTags, !this.options.exportFormFields, 'NoExport')+"</td>";
+        }
+      }
+      this.exportText+="</tr>";
+    }
+    this.exportText+="</thead><tbody>";
+  },
+
+/**
+ * Support function for printVisible().
+ */
+  exportFinish: function() {
+    if (this.hideMsg) this.hideMsg();
+    window.status=Rico.getPhraseById('exportComplete');
+    if (this.exportRows.length > 0) this.exportText+='<tr>'+this.exportRows.join('</tr><tr>')+'</tr>';
+    if (this.exportFooter) this.exportText+=this.exportFooter;
+    this.exportText+="</tbody></table></body></html>";
+    if (this.cancelMenu) this.cancelMenu();
+    var w=window.open('','_blank',this.options.exportWindow);
+    if (w == null) {
+      alert(Rico.getPhraseById('disableBlocker'));
+    } else {
+      w.document.open();
+      w.document.write(this.exportText);
+      w.document.close();
+    }
+    this.exportText=undefined;
+    this.exportRows=undefined;
+  },
+
+/**
+ * Support function for printVisible()
+ */
+  exportStyle: function(elem,styleList) {
+    for (var i=0,s=''; i < styleList.length; i++) {
+      try {
+        var curstyle=Rico.getStyle(elem,styleList[i]);
+        if (curstyle) s+=styleList[i]+':'+curstyle+';';
+      } catch(e) {};
+    }
+    return s;
+  },
+
+/**
+ * Gets the value of the grid cookie and interprets the contents.
+ * All information for a particular grid is stored in a single cookie.
+ * This may include column widths, column hide/show status, current sort, and any column filters.
+ */
+  getCookie: function() {
+    var c=Rico.getCookie(this.options.cookiePrefix+this.tableId);
+    if (!c) return;
+    var cookieVals=c.split(',');
+    for (var i=0; i<cookieVals.length; i++) {
+      var v=cookieVals[i].split(':');
+      if (v.length!=2) continue;
+      var colnum=parseInt(v[0].slice(1),10);
+      if (colnum < 0 || colnum >= this.columns.length) continue;
+      var col=this.columns[colnum];
+      switch (v[0].charAt(0)) {
+        case 'w':
+          col.setColWidth(v[1]);
+          col.customWidth=true;
+          break;
+        case 'h':
+          if (v[1].toLowerCase()=='true')
+            col.hideshow(true,true);
+          else
+            col.hideshow(false,true);
+          break;
+        case 's':
+          if (!this.options.saveColumnInfo.sort || !col.sortable) break;
+          col.setSorted(v[1]);
+          break;
+        case 'f':
+          if (!this.options.saveColumnInfo.filter || !col.filterable) break;
+          var filterTemp=v[1].split('~');
+          col.filterOp=filterTemp.shift();
+          col.filterValues = [];
+          col.filterType = Rico.ColumnConst.USERFILTER;
+          for (var j=0; j<filterTemp.length; j++)
+            col.filterValues.push(unescape(filterTemp[j]));
+          break;
+      }
+    }
+  },
+
+/**
+ * Sets the grid cookie.
+ * All information for a particular grid is stored in a single cookie.
+ * This may include column widths, column hide/show status, current sort, and any column filters.
+ */
+  setCookie: function() {
+    var cookieVals=[];
+    for (var i=0; i<this.columns.length; i++) {
+      var col=this.columns[i];
+      if (this.options.saveColumnInfo.width) {
+        if (col.customWidth) cookieVals.push('w'+i+':'+col.colWidth);
+        if (col.customVisible) cookieVals.push('h'+i+':'+col.visible);
+      }
+      if (this.options.saveColumnInfo.sort) {
+        if (col.currentSort != Rico.ColumnConst.UNSORTED)
+          cookieVals.push('s'+i+':'+col.currentSort);
+      }
+      if (this.options.saveColumnInfo.filter && col.filterType == Rico.ColumnConst.USERFILTER) {
+        var filterTemp=[col.filterOp];
+        for (var j=0; j<col.filterValues.length; j++)
+          filterTemp.push(escape(col.filterValues[j]));
+        cookieVals.push('f'+i+':'+filterTemp.join('~'));
+      }
+    }
+    Rico.setCookie(this.options.cookiePrefix+this.tableId, cookieVals.join(','), this.options.cookieDays, this.options.cookiePath, this.options.cookieDomain);
+  }
+
+};
+
+
+Rico.ColumnConst = {
+  UNFILTERED:   0,
+  SYSTEMFILTER: 1,
+  USERFILTER:   2,
+
+  UNSORTED:  0,
+  SORT_ASC:  "ASC",
+  SORT_DESC: "DESC",
+
+  MINWIDTH: 10
+}
+
+
+/**
+ * @class Define methods that are common to columns in both SimpleGrid and LiveGrid
+ */
+Rico.TableColumnBase = function() {};
+
+Rico.TableColumnBase.prototype = {
+
+/**
+ * Common code used to initialize the column in both SimpleGrid & LiveGrid
+ */
+  baseInit: function(liveGrid,colIdx,hdrInfo,tabIdx) {
+    Rico.log("TableColumnBase.baseInit index="+colIdx+" tabIdx="+tabIdx);
+    this.liveGrid  = liveGrid;
+    this.index     = colIdx;
+    this.hideWidth = Rico.isKonqueror || Rico.isWebKit || liveGrid.headerRowCnt>1 ? 5 : 2;  // column width used for "hidden" columns. Anything less than 5 causes problems with Konqueror. Best to keep this greater than padding used inside cell.
+    this.options   = liveGrid.options;
+    this.tabIdx    = tabIdx;
+    this.hdrCell   = hdrInfo.cell;
+    this.body = document.getElementsByTagName("body")[0];
+    this.displayName  = this.getDisplayName(this.hdrCell);
+    var divs=this.hdrCell.getElementsByTagName('div');
+    this.hdrColDiv=(divs.length<1) ? Rico.wrapChildren(this.hdrCell,'ricoLG_col') : divs[0];
+    this.hdrCellDiv=(divs.length<2) ? Rico.wrapChildren(this.hdrColDiv,'ricoLG_cell') : divs[1];
+    var sectionIndex= tabIdx==0 ? colIdx : colIdx-liveGrid.options.frozenColumns;
+    this.dataCell = liveGrid.tbody[tabIdx].rows[0].cells[sectionIndex];
+    divs=this.dataCell.getElementsByTagName('div');
+    this.dataColDiv=(divs.length<1) ? Rico.wrapChildren(this.dataCell,'ricoLG_col') : divs[0];
+
+    this.mouseDownHandler= Rico.eventHandle(this,'handleMouseDown');
+    this.mouseMoveHandler= Rico.eventHandle(this,'handleMouseMove');
+    this.mouseUpHandler  = Rico.eventHandle(this,'handleMouseUp');
+    this.mouseOutHandler = Rico.eventHandle(this,'handleMouseOut');
+
+    this.format={type:"text"};
+    var spec = liveGrid.options.columnSpecs[colIdx];
+    if (typeof spec == 'object') Rico.extend(this.format, spec);
+    Rico.addClass(this.dataColDiv, this.colClassName());
+    this.visible=true;
+    if (typeof this.format.visible=='boolean') this.visible=this.format.visible;
+    this.sortable     = typeof this.format.canSort=='boolean' ? this.format.canSort : liveGrid.options.canSortDefault;
+    this.currentSort  = Rico.ColumnConst.UNSORTED;
+    this.filterable   = typeof this.format.canFilter=='boolean' ? this.format.canFilter : liveGrid.options.canFilterDefault;
+    this.filterType   = Rico.ColumnConst.UNFILTERED;
+    this.hideable     = typeof this.format.canHide=='boolean' ? this.format.canHide : liveGrid.options.canHideDefault;
+
+    var wi;
+    switch (typeof this.format.width) {
+      case 'number': wi=this.format.width; break;
+      case 'string': wi=parseInt(this.format.width,10); break;
+      default:       wi=hdrInfo.initWidth; break;
+    }
+    wi=(typeof(wi)=='number') ? Math.max(wi,Rico.ColumnConst.MINWIDTH) : liveGrid.options.defaultWidth;
+    this.setColWidth(wi);
+    if (!this.visible) this.setDisplay('none');
+  },
+  
+  colClassName: function() {
+    return this.format.ClassName ? this.format.ClassName : this.liveGrid.tableId+'_col'+this.index;
+  },
+
+  insertResizer: function() {
+    //this.hdrCell.style.width='';
+    if (this.format.noResize) return;
+    var resizer=document.createElement('div');
+    resizer.className='ricoLG_Resize';
+    resizer.style[this.liveGrid.align[1]]='0px';
+    this.hdrCellDiv.appendChild(resizer);
+    Rico.eventBind(resizer,"mousedown", this.mouseDownHandler, false);
+  },
+
+/**
+ * get the display name of a column
+ */
+  getDisplayName: function(el) {
+    var anchors=el.getElementsByTagName("A");
+    var s=anchors.length > 0 ? anchors[0].innerHTML : Rico.stripTags(el.innerHTML);
+    return Rico.trim(s);
+  },
+
+  _clear: function(gridCell) {
+    gridCell.innerHTML='&nbsp;';
+  },
+
+  clearCell: function(rowIndex) {
+    var gridCell=this.cell(rowIndex);
+    this._clear(gridCell,rowIndex);
+    if (this.liveGrid.buffer && this.liveGrid.buffer.options.acceptStyle) gridCell.style.cssText='';
+  },
+
+  dataTable: function() {
+    return this.liveGrid.tabs[this.tabIdx];
+  },
+
+  numRows: function() {
+    return this.dataColDiv.childNodes.length;
+  },
+
+  clearColumn: function() {
+    var childCnt=this.numRows();
+    for (var r=0; r<childCnt; r++)
+      this.clearCell(r);
+  },
+
+  cell: function(r) {
+    return this.dataColDiv.childNodes[r];
+  },
+
+  getFormattedValue: function(r,xImg,xForm,xClass) {
+    return Rico.getInnerText(this.cell(r),xImg,xForm,xClass);
+  },
+
+  setColWidth: function(wi) {
+    if (typeof wi=='number') {
+      wi=parseInt(wi,10);
+      if (wi < Rico.ColumnConst.MINWIDTH) return;
+      wi=wi+'px';
+    }
+    Rico.log('setColWidth '+this.index+': '+wi);
+    this.colWidth=wi;
+    this.hdrColDiv.style.width=wi;
+    this.dataColDiv.style.width=wi;
+  },
+
+  pluginMouseEvents: function() {
+    if (this.mousePluggedIn==true) return;
+    Rico.eventBind(this.body,"mousemove", this.mouseMoveHandler, false);
+    Rico.eventBind(this.body,"mouseup",   this.mouseUpHandler  , false);
+    Rico.eventBind(this.body,"mouseout",  this.mouseOutHandler , false);
+    this.mousePluggedIn=true;
+  },
+
+  unplugMouseEvents: function() {
+    Rico.eventUnbind(this.body,"mousemove", this.mouseMoveHandler, false);
+    Rico.eventUnbind(this.body,"mouseup",   this.mouseUpHandler  , false);
+    Rico.eventUnbind(this.body,"mouseout",  this.mouseOutHandler , false);
+    this.mousePluggedIn=false;
+  },
+
+  handleMouseDown: function(e) {
+    this.resizeStart=Rico.eventClient(e).x;
+    this.origWidth=parseInt(this.colWidth,10);
+    var p=Rico.positionedOffset(this.hdrCell);
+    if (this.liveGrid.direction=='rtl') {
+      this.edge=p.left;
+      switch (this.tabIdx) {
+        case 0: this.edge+=this.liveGrid.innerDiv.offsetWidth; break;
+        case 1: this.edge-=this.liveGrid.scrollDiv.scrollLeft; break;
+      }
+    } else {
+      this.edge=p.left+this.hdrCell.offsetWidth;
+      if (this.tabIdx>0) this.edge+=Rico.nan2zero(this.liveGrid.tabs[0].offsetWidth);
+    }
+    this.liveGrid.resizeDiv.style.left=this.edge+"px";
+    this.liveGrid.resizeDiv.style.display="";
+    this.liveGrid.outerDiv.style.cursor='e-resize';
+    this.tmpHighlight=this.liveGrid.highlightEnabled;
+    this.liveGrid.highlightEnabled=false;
+    this.pluginMouseEvents();
+    Rico.eventStop(e);
+  },
+
+  handleMouseMove: function(e) {
+    var delta=Rico.eventClient(e).x-this.resizeStart;
+    var newWidth=(this.liveGrid.direction=='rtl') ? this.origWidth-delta : this.origWidth+delta;
+    if (newWidth < Rico.ColumnConst.MINWIDTH) return;
+    this.liveGrid.resizeDiv.style.left=(this.edge+delta)+"px";
+    this.colWidth=newWidth;
+    Rico.eventStop(e);
+  },
+
+  handleMouseUp: function(e) {
+    this.unplugMouseEvents();
+    Rico.log('handleMouseUp '+this.liveGrid.tableId);
+    this.liveGrid.outerDiv.style.cursor='';
+    this.liveGrid.resizeDiv.style.display="none";
+    this.setColWidth(this.colWidth);
+    this.customWidth=true;
+    this.liveGrid.setCookie();
+    this.liveGrid.highlightEnabled=this.tmpHighlight;
+    this.liveGrid.sizeDivs();
+    Rico.eventStop(e);
+  },
+
+  handleMouseOut: function(e) {
+    var reltg = Rico.eventRelatedTarget(e) || e.toElement;
+    while (reltg != null && reltg.nodeName.toLowerCase() != 'body')
+      reltg=reltg.parentNode;
+    if (reltg!=null && reltg.nodeName.toLowerCase() == 'body') return true;
+    this.handleMouseUp(e);
+    return true;
+  },
+
+  setDisplay: function(d) {
+    this.hdrCell.style.display=d;
+    this.hdrColDiv.style.display=d;
+    this.dataCell.style.display=d;
+    this.dataColDiv.style.display=d;
+  },
+  
+  hideshow: function(visible,noresize) {
+    this.setDisplay(visible ? '' : 'none');
+    this.liveGrid.cancelMenu();
+    this.visible=visible;
+    this.customVisible=true;
+    if (noresize) return;
+    this.liveGrid.setCookie();
+    this.liveGrid.sizeDivs();
+  },
+
+  hideColumn: function() {
+    Rico.log('hideColumn '+this.liveGrid.tableId);
+    this.hideshow(false,false);
+  },
+
+  showColumn: function() {
+    Rico.log('showColumn '+this.liveGrid.tableId);
+    this.hideshow(true,false);
+  },
+
+  chooseColumn: function(e) {
+    var elem=Rico.eventElement(e);
+    this.hideshow(elem.checked,false);
+  },
+
+  setImage: function() {
+    if ( this.currentSort == Rico.ColumnConst.SORT_ASC ) {
+       this.imgSort.style.display='inline-block';
+       this.imgSort.className=Rico.theme.sortAsc || 'rico-icon ricoLG_sortAsc';
+    } else if ( this.currentSort == Rico.ColumnConst.SORT_DESC ) {
+       this.imgSort.style.display='inline-block';
+       this.imgSort.className=Rico.theme.sortDesc || 'rico-icon ricoLG_sortDesc';
+    } else {
+       this.imgSort.style.display='none';
+    }
+    if (this.filterType == Rico.ColumnConst.USERFILTER) {
+       this.imgFilter.style.display='inline-block';
+       this.imgFilter.title=this.getFilterText();
+    } else {
+       this.imgFilter.style.display='none';
+    }
+  },
+
+  canHideShow: function() {
+    return this.hideable;
+  }
+
+};
diff --git a/lib/rico3/minsrc/ricoLiveGrid.js b/lib/rico3/minsrc/ricoLiveGrid.js
new file mode 100644 (file)
index 0000000..c134ac6
--- /dev/null
@@ -0,0 +1,2444 @@
+/*
+ *  (c) 2005-2011 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+/** @namespace */
+if (!Rico.Buffer) Rico.Buffer = {};
+
+Rico.Buffer.Base = function(dataTable, options) {
+  this.initialize(dataTable, options);
+}
+/** @lends Rico.Buffer.Base# */
+Rico.Buffer.Base.prototype = {
+/**
+ * @class Defines the static buffer class (no AJAX).
+ * Loads buffer with data that already exists in the document as an HTML table or passed via javascript.
+ * Also serves as a base class for AJAX-enabled buffers.
+ * @constructs
+ */
+  initialize: function(dataTable, options) {
+    this.clear();
+    this.updateInProgress = false;
+    this.lastOffset = 0;
+    this.rcvdRowCount = false;  // true if an eof element was included in the last response
+    this.foundRowCount = false; // true if a response is ever received with eof true
+    this.totalRows = 0;
+    this.rowcntContent = "";
+    this.rcvdOffset = -1;
+    this.options = {
+      fixedHdrRows     : 0,
+      canFilter        : true,  // does buffer object support filtering?
+      isEncoded        : true,  // is the data received via ajax html encoded?
+      acceptStyle      : false, // copy style from original/ajax data?
+      canRefresh       : false  // should "refresh" be shown on filter menu?
+    };
+    Rico.extend(this.options, options || {});
+    if (dataTable) {
+      this.loadRowsFromTable(dataTable,this.options.fixedHdrRows);
+      dataTable.parentNode.removeChild(dataTable);  // delete the data once it has been loaded
+    }
+  },
+
+  registerGrid: function(liveGrid) {
+    this.liveGrid = liveGrid;
+  },
+
+  setTotalRows: function( newTotalRows ) {
+    if (typeof(newTotalRows)!='number') newTotalRows=this.size;
+    if (this.totalRows == newTotalRows) return;
+    this.totalRows = newTotalRows;
+    if (!this.liveGrid) return;
+    Rico.log("setTotalRows, newTotalRows="+newTotalRows);
+    switch (this.liveGrid.sizeTo) {
+      case 'data':
+        this.liveGrid.resizeWindow();
+        break;
+      case 'datamax':
+        this.liveGrid.setPageSize(newTotalRows);
+        break;
+      default:
+        this.liveGrid.updateHeightDiv();
+        break;
+    }
+  },
+
+  loadRowsFromTable: function(tableElement,firstRow) {
+    var newRows = [];
+    var trs = tableElement.getElementsByTagName("tr");
+    for ( var i=firstRow || 0; i < trs.length; i++ ) {
+      var row = [];
+      var cells = trs[i].getElementsByTagName("td");
+      for ( var j=0; j < cells.length ; j++ )
+        row[j]=cells[j].innerHTML;
+      newRows.push( row );
+    }
+    this.loadRows(newRows);
+  },
+
+  loadRowsFromArray: function(array2D) {
+    for ( var i=0; i < array2D.length; i++ ) {
+      for ( var j=0; j < array2D[i].length ; j++ ) {
+        array2D[i][j]=array2D[i][j].toString();
+      }
+    }
+    this.loadRows(array2D);
+  },
+
+  loadRows: function(jstable) {
+    this.baseRows = jstable;
+    this.startPos = 0;
+    this.size = this.baseRows.length;
+  },
+
+  dom2jstable: function(rowsElement) {
+    Rico.log('dom2jstable: encoded='+this.options.isEncoded);
+    var newRows = [];
+    var trs = rowsElement.getElementsByTagName("tr");
+    for ( var i=0; i < trs.length; i++ ) {
+      var row = [];
+      var cells = trs[i].getElementsByTagName("td");
+      for ( var j=0; j < cells.length ; j++ )
+        row[j]=Rico.getContentAsString(cells[j],this.options.isEncoded);
+      newRows.push( row );
+    }
+    return newRows;
+  },
+
+  _blankRow: function() {
+    var newRow=[];
+    for (var i=0; i<this.liveGrid.columns.length; i++) {
+      newRow[i]='';
+    }
+    return newRow;
+  },
+
+  deleteRows: function(rowIndex,cnt) {
+    this.baseRows.splice(rowIndex,typeof(cnt)=='number' ? cnt : 1);
+    this.liveGrid.isPartialBlank=true;
+    this.size=this.baseRows.length;
+  },
+
+  insertRow: function(beforeRowIndex) {
+    var r=this._blankRow();
+    this.baseRows.splice(beforeRowIndex,0,r);
+    this.size=this.baseRows.length;
+    this.liveGrid.isPartialBlank=true;
+    if (this.startPos < 0) this.startPos=0;
+    return r;
+  },
+
+  appendRows: function(cnt) {
+    var newRows=[];
+    for (var i=0; i<cnt; i++) {
+      var r=this._blankRow();
+      this.baseRows.push(r);
+      newRows.push(r);
+    }
+    this.size=this.baseRows.length;
+    this.liveGrid.isPartialBlank=true;
+    if (this.startPos < 0) this.startPos=0;
+    return newRows;
+  },
+  
+  sortFunc: function(coltype) {
+    var self=this;
+    switch (coltype) {
+      case 'number': return function(a,b) { return self._sortNumeric(a,b); };
+      case 'control':return function(a,b) { return self._sortControl(a,b); };
+      default:       return function(a,b) { return self._sortAlpha(a,b); };
+    }
+  },
+
+  sortBuffer: function(colnum) {
+    if (!this.baseRows) {
+      this.delayedSortCol=colnum;
+      return;
+    }
+    this.liveGrid.showMsg(Rico.getPhraseById("sorting"));
+    this.sortColumn=colnum;
+    var col=this.liveGrid.columns[colnum];
+    this.getValFunc=col._sortfunc;
+    this.baseRows.sort(this.sortFunc(col.format.type));
+    if (col.getSortDirection()=='DESC') this.baseRows.reverse();
+  },
+  
+  _sortAlpha: function(a,b) {
+    var aa = this.sortColumn<a.length ? Rico.getInnerText(a[this.sortColumn]) : '';
+    var bb = this.sortColumn<b.length ? Rico.getInnerText(b[this.sortColumn]) : '';
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+  },
+
+  _sortNumeric: function(a,b) {
+    var aa = this.sortColumn<a.length ? this.nan2zero(Rico.getInnerText(a[this.sortColumn])) : 0;
+    var bb = this.sortColumn<b.length ? this.nan2zero(Rico.getInnerText(b[this.sortColumn])) : 0;
+    return aa-bb;
+  },
+
+  nan2zero: function(n) {
+    if (typeof(n)=='string') n=parseFloat(n);
+    return isNaN(n) || typeof(n)=='undefined' ? 0 : n;
+  },
+  
+  _sortControl: function(a,b) {
+    var aa = this.sortColumn<a.length ? Rico.getInnerText(a[this.sortColumn]) : '';
+    var bb = this.sortColumn<b.length ? Rico.getInnerText(b[this.sortColumn]) : '';
+    if (this.getValFunc) {
+      aa=this.getValFunc(aa);
+      bb=this.getValFunc(bb);
+    }
+    if (aa==bb) return 0;
+    if (aa<bb) return -1;
+    return 1;
+  },
+
+  clear: function() {
+    this.baseRows = [];
+    this.rows = [];
+    this.modified = [];
+    this.attr = null;
+    this.startPos = -1;
+    this.size = 0;
+    this.windowPos = 0;
+  },
+
+  isInRange: function(position) {
+    var lastRow=Math.min(this.totalRows, position + this.liveGrid.pageSize);
+    return (position >= this.startPos) && (lastRow <= this.endPos()); // && (this.size != 0);
+  },
+
+  endPos: function() {
+    return this.startPos + this.rows.length;
+  },
+
+  fetch: function(offset) {
+    Rico.log('fetch '+this.liveGrid.tableId+': offset='+offset);
+    this.applyFilters();
+    this.setTotalRows();
+    this.rcvdRowCount = true;
+    this.foundRowCount = true;
+    if (offset < 0) offset=0;
+    this.liveGrid.refreshContents(offset);
+    return;
+  },
+
+/**
+ * @return a 2D array of buffer data representing the rows that are currently visible on the grid
+ */
+  visibleRows: function() {
+    return this.rows.slice(this.windowStart,this.windowEnd);
+  },
+
+  setWindow: function(startrow, endrow) {
+    this.windowStart = startrow - this.startPos;  // position in the buffer of first visible row
+    Rico.log('setWindow '+this.liveGrid.tableId+': '+startrow+', '+endrow+', newstart='+this.windowStart);
+    this.windowEnd = Math.min(endrow,this.size);  // position in the buffer of last visible row containing data+1
+    this.windowPos = startrow;                    // position in the dataset of first visible row
+  },
+
+/**
+ * @return true if bufRow is currently visible in the grid
+ */
+  isVisible: function(bufRow) {
+    return bufRow < this.rows.length && bufRow >= this.windowStart && bufRow < this.windowEnd;
+  },
+  
+/**
+ * takes a window row index and returns the corresponding buffer row index
+ */
+  bufferRow: function(windowRow) {
+    return this.windowStart+windowRow;
+  },
+
+/**
+ * @return buffer cell at the specified visible row/col index
+ */
+  getWindowCell: function(windowRow,col) {
+    var bufrow=this.bufferRow(windowRow);
+    return this.isVisible(bufrow) && col < this.rows[bufrow].length ? this.rows[bufrow][col] : null;
+  },
+
+  getWindowStyle: function(windowRow,col) {
+    var bufrow=this.bufferRow(windowRow);
+    return this.attr && this.isVisible(bufrow) && this.attr[bufrow] && col < this.attr[bufrow].length ? this.attr[bufrow][col] : '';
+  },
+
+  getWindowValue: function(windowRow,col) {
+    return this.getWindowCell(windowRow,col);
+  },
+
+  setWindowValue: function(windowRow,col,newval) {
+    var bufrow=this.bufferRow(windowRow);
+    if (bufrow >= this.windowEnd) return false;
+    return this.setValue(bufrow,col,newval);
+  },
+
+  getCell: function(bufRow,col) {
+    return bufRow < this.size ? this.rows[bufRow][col] : null;
+  },
+
+  getValue: function(bufRow,col) {
+    return this.getCell(bufRow,col);
+  },
+
+  setValue: function(bufRow,col,newval,newstyle) {
+    if (bufRow>=this.size) return false;
+    if (!this.rows[bufRow][col]) this.rows[bufRow][col]={};
+    this.rows[bufRow][col]=newval;
+    if (this.options.acceptStyle && typeof newstyle=='string') {
+      if (!this.attr) this.attr=[];
+      if (!this.attr[bufRow]) this.attr[bufRow]=[];
+      this.attr[bufRow][col]=newstyle;
+    }
+    if (!this.modified[bufRow]) this.modified[bufRow]=[];
+    this.modified[bufRow][col]=true;
+    return true;
+  },
+
+  getRows: function(start, count) {
+    var begPos = start - this.startPos;
+    var endPos = Math.min(begPos + count,this.size);
+    var results = [];
+    for ( var i=begPos; i < endPos; i++ ) {
+      results.push(this.rows[i]);
+    }
+    return results;
+  },
+
+  applyFilters: function() {
+    var newRows=[],re=[];
+    var r,c,n,i,showRow,filtercnt;
+    var cols=this.liveGrid.columns;
+    for (n=0,filtercnt=0; n<cols.length; n++) {
+      c=cols[n];
+      if (c.filterType == Rico.ColumnConst.UNFILTERED) continue;
+      filtercnt++;
+      if (c.filterOp=='LIKE') re[n]=new RegExp(c.filterValues[0],'i');
+    }
+    Rico.log('applyFilters: # of filters='+filtercnt);
+    if (filtercnt==0) {
+      this.rows = this.baseRows;
+    } else {
+      for (r=0; r<this.baseRows.length; r++) {
+        showRow=true;
+        for (n=0; n<cols.length && showRow; n++) {
+          c=cols[n];
+          if (c.filterType == Rico.ColumnConst.UNFILTERED) continue;
+          switch (c.filterOp) {
+            case 'LIKE':
+              showRow=re[n].test(this.baseRows[r][n]);
+              break;
+            case 'EQ':
+              showRow=this.baseRows[r][n]==c.filterValues[0];
+              break;
+            case 'NE':
+              for (i=0; i<c.filterValues.length && showRow; i++)
+                showRow=this.baseRows[r][n]!=c.filterValues[i];
+              break;
+            case 'LE':
+              if (c.format.type=='number')
+                showRow=this.nan2zero(this.baseRows[r][n])<=this.nan2zero(c.filterValues[0]);
+              else
+                showRow=this.baseRows[r][n]<=c.filterValues[0];
+              break;
+            case 'GE':
+              if (c.format.type=='number')
+                showRow=this.nan2zero(this.baseRows[r][n])>=this.nan2zero(c.filterValues[0]);
+              else
+                showRow=this.baseRows[r][n]>=c.filterValues[0];
+              break;
+            case 'NULL':
+              showRow=this.baseRows[r][n]=='';
+              break;
+            case 'NOTNULL':
+              showRow=this.baseRows[r][n]!='';
+              break;
+          }
+        }
+        if (showRow) newRows.push(this.baseRows[r]);
+      }
+      this.rows = newRows;
+    }
+    this.rowcntContent = this.size = this.rows.length;
+  },
+
+  printAll: function() {
+    this.liveGrid.showMsg(Rico.getPhraseById('exportInProgress'));
+    Rico.runLater(10,this,'_printAll');  // allow message to paint
+  },
+
+/**
+ * Support function for printAll()
+ */
+  _printAll: function() {
+    this.liveGrid.exportStart();
+    this.exportBuffer(this.getRows(0,this.totalRows));
+    this.liveGrid.exportFinish();
+  },
+
+/**
+ * Copies visible rows to a new window as a simple html table.
+ */
+  printVisible: function() {
+    this.liveGrid.showMsg(Rico.getPhraseById('exportInProgress'));
+    Rico.runLater(10,this,'_printVisible');  // allow message to paint
+  },
+
+  _printVisible: function() {
+    this.liveGrid.exportStart();
+    this.exportBuffer(this.visibleRows());
+    this.liveGrid.exportFinish();
+  },
+
+/**
+ * Send all rows to print/export window
+ */
+  exportBuffer: function(rows,startPos) {
+    var r,c,v,col,exportText;
+    Rico.log("exportBuffer: "+rows.length+" rows");
+    var exportStyles=this.liveGrid.getExportStyles(this.liveGrid.tbody[0]);
+    var tdstyle=[];
+    var totalcnt=startPos || 0;
+    var cols=this.liveGrid.columns;
+    for (c=0; c<cols.length; c++) {
+      if (cols[c].visible) tdstyle[c]=this.liveGrid.exportStyle(cols[c].cell(0),exportStyles);  // assumes row 0 style applies to all rows
+    }
+    for(r=0; r < rows.length; r++) {
+      exportText='';
+      for (c=0; c<cols.length; c++) {
+        if (!cols[c].visible) continue;
+        col=cols[c];
+        col.expStyle=tdstyle[c];
+        v=col._export(rows[r][c],rows[r]);
+        if (v=='') v='&nbsp;';
+        exportText+="<td style='"+col.expStyle+"'>"+v+"</td>";
+      }
+      this.liveGrid.exportRows.push(exportText);
+      totalcnt++;
+      if (totalcnt % 10 == 0) window.status=Rico.getPhraseById('exportStatus',totalcnt);
+    }
+  }
+
+};
+
+
+// Rico.LiveGrid -----------------------------------------------------
+
+Rico.LiveGrid = function(tableId, buffer, options) {
+  this.initialize(tableId, buffer, options);
+}
+
+/** 
+ * @lends Rico.LiveGrid#
+ * @property tableId id string for this grid
+ * @property options the options object passed to the constructor extended with defaults
+ * @property buffer the buffer object containing the data for this grid
+ * @property columns array of {@link Rico.LiveGridColumn} objects
+ */
+Rico.LiveGrid.prototype = {
+/**
+ * @class Buffered LiveGrid component
+ * @extends Rico.GridCommon
+ * @constructs
+ */
+  initialize: function( tableId, buffer, options ) {
+    Rico.extend(this, Rico.GridCommon);
+    Rico.extend(this, Rico.LiveGridMethods);
+    this.baseInit();
+    this.tableId = tableId;
+    this.buffer = buffer;
+    this.actionId='_action_'+tableId;
+    Rico.setDebugArea(tableId+"_debugmsgs");    // if used, this should be a textarea
+
+    Rico.extend(this.options, {
+      visibleRows      : -3,    // -1 or 'window'=size grid to client window; -2 or 'data'=size grid to min(window,data); -3 or 'body'=size so body does not have a scrollbar; -4 or 'parent'=size to parent element (e.g. if grid is inside a div)
+      frozenColumns    : 0,
+      offset           : 0,     // first row to be displayed
+      prefetchBuffer   : true,  // load table on page load?
+      minPageRows      : 2,
+      maxPageRows      : 50,
+      canSortDefault   : true,  // can be overridden in the column specs
+      canFilterDefault : buffer.options.canFilter, // can be overridden in the column specs
+      canHideDefault   : true,  // can be overridden in the column specs
+
+      // highlight & selection parameters
+      highlightElem    : 'none',// what gets highlighted/selected (cursorRow, cursorCell, menuRow, menuCell, selection, or none)
+      highlightSection : 3,     // which section gets highlighted (frozen=1, scrolling=2, all=3, none=0)
+      highlightMethod  : 'class', // outline, class, both (outline is less CPU intensive on the client)
+      highlightClass   : Rico.theme.gridHighlightClass || 'ricoLG_selection',
+
+      // export/print parameters
+      maxPrint         : 5000,  // max # of rows that can be printed/exported, 0=disable print/export feature
+
+      // heading parameters
+      headingSort      : 'link', // link: make headings a link that will sort column, hover: make headings a hoverset, none: events on headings are disabled
+      hdrIconsFirst    : true    // true: put sort & filter icons before header text, false: after
+    });
+    // other options:
+    //   sortCol: initial sort column
+
+    var self=this;
+    this.options.sortHandler = function() { self.sortHandler(); };
+    this.options.filterHandler = function() { self.filterHandler(); };
+    this.options.onRefreshComplete = function(firstrow,lastrow) { self.bookmarkHandler(firstrow,lastrow); };
+    this.options.rowOverHandler = Rico.eventHandle(this,'rowMouseOver');
+    this.options.mouseDownHandler = Rico.eventHandle(this,'selectMouseDown');
+    this.options.mouseOverHandler = Rico.eventHandle(this,'selectMouseOver');
+    this.options.mouseUpHandler  = Rico.eventHandle(this,'selectMouseUp');
+    Rico.extend(this.options, options || {});
+
+    switch (typeof this.options.visibleRows) {
+      case 'string':
+        this.sizeTo=this.options.visibleRows;
+        switch (this.options.visibleRows) {
+          case 'data':   this.options.visibleRows=-2; break;
+          case 'body':   this.options.visibleRows=-3; break;
+          case 'parent': this.options.visibleRows=-4; break;
+          case 'datamax':this.options.visibleRows=-5; break;
+          default:       this.options.visibleRows=-1; break;
+        }
+        break;
+      case 'number':
+        switch (this.options.visibleRows) {
+          case -1: this.sizeTo='window'; break;
+          case -2: this.sizeTo='data'; break;
+          case -3: this.sizeTo='body'; break;
+          case -4: this.sizeTo='parent'; break;
+          case -5: this.sizeTo='datamax'; break;
+          default: this.sizeTo='fixed'; break;
+        }
+        break;
+      default:
+        this.sizeTo='body';
+        this.options.visibleRows=-3;
+        break;
+    }
+    this.highlightEnabled=this.options.highlightSection>0;
+    this.pageSize=0;
+    this.createTables();
+    if (this.headerColCnt==0) {
+      alert('ERROR: no columns found in "'+this.tableId+'"');
+      return;
+    }
+    this.createColumnArray('LiveGridColumn');
+    if (this.options.headingSort=='hover')
+      this.createHoverSet();
+
+    this.bookmark=document.getElementById(this.tableId+"_bookmark");
+    this.sizeDivs();
+    var filterUIrow=-1;
+    if (this.buffer.options.canFilter && this.options.AutoFilter)
+      filterUIrow=this.addHeadingRow('ricoLG_FilterRow');
+    this.createDataCells(this.options.visibleRows);
+    if (this.pageSize == 0) return;
+    this.buffer.registerGrid(this);
+    if (this.buffer.setBufferSize) this.buffer.setBufferSize(this.pageSize);
+    this.scrollTimeout = null;
+    this.lastScrollPos = 0;
+    this.attachMenuEvents();
+
+    this.setSortUI( this.options.sortCol, this.options.sortDir );
+    this.setImages();
+    if (this.listInvisible().length==this.columns.length)
+      this.columns[0].showColumn();
+    this.sizeDivs();
+    this.scrollDiv.style.display="";
+    if (this.buffer.totalRows>0)
+      this.updateHeightDiv();
+    if (this.options.prefetchBuffer) {
+      if (this.bookmark) this.bookmark.innerHTML = Rico.getPhraseById('bookmarkLoading');
+      if (this.options.canFilterDefault && this.options.getQueryParms)
+        this.checkForFilterParms();
+      this.scrollToRow(this.options.offset);
+      this.buffer.fetch(this.options.offset);
+    }
+    if (filterUIrow >= 0)
+      this.createFilters(filterUIrow);
+    this.scrollEventFunc=Rico.eventHandle(this,'handleScroll');
+    this.wheelEventFunc=Rico.eventHandle(this,'handleWheel');
+    this.wheelEvent=(Rico.isIE || Rico.isOpera || Rico.isWebKit) ? 'mousewheel' : 'DOMMouseScroll';
+    if (this.options.offset && this.options.offset < this.buffer.totalRows)
+      Rico.runLater(50,this,'scrollToRow',this.options.offset);  // Safari requires a delay
+    this.pluginScroll();
+    this.setHorizontalScroll();
+    Rico.log("setHorizontalScroll done");
+    if (this.options.windowResize)
+      Rico.runLater(100,this,'pluginWindowResize');
+    Rico.log("initialize complete for "+this.tableId);
+    //alert('clientLeft='+this.scrollDiv.clientLeft);
+    if (this.direction=='rtl' && (!Rico.isWebKit || this.scrollDiv.clientLeft > 0)) {
+      this.scrollTab.style.right='0px';
+    } else {
+      this.scrollTab.style.left='0px';
+      Rico.setStyle(this.tabs[1], {'float': 'left'});
+    }
+  }
+};
+
+
+Rico.LiveGridMethods = {
+/** @lends Rico.LiveGrid# */
+
+  createHoverSet: function() {
+    var hdrs=[];
+    for( var c=0; c < this.headerColCnt; c++ ) {
+      if (this.columns[c].sortable) {\r
+        hdrs.push(this.columns[c].hdrCellDiv);
+      }
+    }
+    this.hoverSet = new Rico.HoverSet(hdrs);
+  },
+
+  checkForFilterParms: function() {
+    var s=window.location.search;
+    if (s.charAt(0)=='?') s=s.substring(1);
+    var pairs = s.split('&');
+    for (var i=0; i<pairs.length; i++) {
+      if (pairs[i].match(/^f\[\d+\]/)) {
+        this.buffer.options.requestParameters.push(pairs[i]);
+      }
+    }
+  },
+
+/**
+ * Refreshes a detail grid from a master grid
+ * @returns row index on master table on success, -1 otherwise
+ */
+  drillDown: function(e,masterColNum,detailColNum) {
+    var cell=Rico.eventElement(e || window.event);
+    cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+    if (!cell) return -1;
+    var idx=this.winCellIndex(cell);
+    if (idx.row >= this.buffer.totalRows) return -1
+    this.unhighlight();
+    this.menuIdx=idx;  // ensures selection gets cleared when menu is displayed
+    this.highlight(idx);
+    var drillValue=this.buffer.getWindowCell(idx.row,masterColNum);
+    for (var i=3; i<arguments.length; i++)
+      arguments[i].setDetailFilter(detailColNum,drillValue);
+    return idx.row;
+  },
+
+/**
+ * set filter on a detail grid that is in a master-detail relationship
+ */
+  setDetailFilter: function(colNumber,filterValue) {
+    var c=this.columns[colNumber];
+    c.format.ColData=filterValue;
+    c.setSystemFilter('EQ',filterValue);
+  },
+
+/**
+ * Create one table for frozen columns and one for scrolling columns.
+ * Also create div's to contain them.
+ * @returns true on success
+ */
+  createTables: function() {
+    var insertloc,hdrSrc,i;
+    var table = document.getElementById(this.tableId) || document.getElementById(this.tableId+'_outerDiv');
+    if (!table) return false;
+    if (table.tagName.toLowerCase()=='table') {
+      var theads=table.getElementsByTagName("thead");
+      if (theads.length == 1) {
+        Rico.log("createTables: using thead section, id="+this.tableId);
+        if (this.options.ColGroupsOnTabHdr && this.options.ColGroups) {
+          var r=theads[0].insertRow(0);
+          this.insertPanelNames(r, 0, this.options.frozenColumns, 'ricoFrozen');
+          this.insertPanelNames(r, this.options.frozenColumns, this.options.columnSpecs.length);
+        }
+        hdrSrc=theads[0].rows;
+      } else {
+        Rico.log("createTables: using tbody section, id="+this.tableId);
+        hdrSrc=new Array(table.rows[0]);
+      }
+      insertloc=table;
+    } else if (this.options.columnSpecs.length > 0) {
+      if (!table.id.match(/_outerDiv$/)) insertloc=table;
+      Rico.log("createTables: inserting at "+table.tagName+", id="+this.tableId);
+    } else {
+      alert("ERROR!\n\nUnable to initialize '"+this.tableId+"'\n\nLiveGrid terminated");
+      return false;
+    }
+
+    this.createDivs();
+    this.scrollContainer = this.createDiv("scrollContainer",this.structTabLR);
+    this.scrollContainer.appendChild(this.scrollDiv); // move scrollDiv
+    this.scrollTab = this.createDiv("scrollTab",this.scrollContainer);
+    this.shadowDiv  = this.createDiv("shadow",this.scrollDiv);
+    this.shadowDiv.style.direction='ltr';  // avoid FF bug
+    this.scrollDiv.style.display="none";
+    this.scrollDiv.scrollTop=0;
+    if (this.options.highlightMethod!='class') {
+      this.highlightDiv=[];
+      switch (this.options.highlightElem) {
+        case 'menuRow':
+        case 'cursorRow':
+          this.highlightDiv[0] = this.createDiv("highlight",this.outerDiv);
+          this.highlightDiv[0].style.display="none";
+          break;
+        case 'menuCell':
+        case 'cursorCell':
+          for (i=0; i<2; i++) {
+            this.highlightDiv[i] = this.createDiv("highlight",i==0 ? this.frozenTabs : this.scrollTab);
+            this.highlightDiv[i].style.display="none";
+            this.highlightDiv[i].id+=i;
+          }
+          break;
+        case 'selection':
+          // create one div for each side of the rectangle
+          var parentDiv=this.options.highlightSection==1 ? this.frozenTabs : this.scrollTab;
+          for (i=0; i<4; i++) {
+            this.highlightDiv[i] = this.createDiv("highlight",parentDiv);
+            this.highlightDiv[i].style.display="none";
+            this.highlightDiv[i].style.overflow="hidden";
+            this.highlightDiv[i].id+=i;
+            this.highlightDiv[i].style[i % 2==0 ? 'height' : 'width']="0px";
+          }
+          break;
+      }
+    }
+
+    // create new tables
+    for (i=0; i<3; i++) {
+      this.tabs[i] = document.createElement("table");
+      this.tabs[i].className = (i < 2) ? 'ricoLG_table' : 'ricoLG_scrollTab';
+      this.tabs[i].border=0;
+      this.tabs[i].cellPadding=0;
+      this.tabs[i].cellSpacing=0;
+      this.tabs[i].id = this.tableId+"_tab"+i;
+    }
+    // set headings
+    for (i=0; i<2; i++) {
+      this.thead[i]=this.tabs[i].createTHead();
+      this.thead[i].className='ricoLG_top';
+      if (Rico.theme.gridheader) Rico.addClass(this.thead[i],Rico.theme.gridheader);
+    }
+    // set bodies
+    for (i=0; i<2; i++) {
+      this.tbody[i]=Rico.getTBody(this.tabs[i==0?0:2]);
+      this.tbody[i].className='ricoLG_bottom';
+      if (Rico.theme.gridcontent) Rico.addClass(this.tbody[i],Rico.theme.gridcontent);
+      this.tbody[i].insertRow(-1);
+    }
+    this.frozenTabs.appendChild(this.tabs[0]);
+    this.innerDiv.appendChild(this.tabs[1]);
+    this.scrollTab.appendChild(this.tabs[2]);
+    if (insertloc) insertloc.parentNode.insertBefore(this.outerDiv,insertloc);
+    if (hdrSrc) {
+      this.headerColCnt = this.getColumnInfo(hdrSrc);
+      this.loadHdrSrc(hdrSrc);
+    } else {
+      this.createHdr(0,0,this.options.frozenColumns);
+      this.createHdr(1,this.options.frozenColumns,this.options.columnSpecs.length);
+      if (this.options.ColGroupsOnTabHdr && this.options.ColGroups) {
+        this.insertPanelNames(this.thead[0].insertRow(0), 0, this.options.frozenColumns);
+        this.insertPanelNames(this.thead[1].insertRow(0), this.options.frozenColumns, this.options.columnSpecs.length);
+      }
+      for (i=0; i<2; i++)
+        this.headerColCnt = this.getColumnInfo(this.thead[i].rows);
+    }
+    for( var c=0; c < this.headerColCnt; c++ )
+      this.tbody[c<this.options.frozenColumns ? 0 : 1].rows[0].insertCell(-1);
+    if (insertloc) table.parentNode.removeChild(table);
+    Rico.log('createTables end');
+    return true;
+  },
+
+  createDataCells: function(visibleRows) {
+    if (visibleRows < 0) {
+      for (var i=0; i<this.options.minPageRows; i++)
+        this.appendBlankRow();
+      this.sizeDivs();
+      this.autoAppendRows(this.remainingHt());
+    } else {
+      for( var r=0; r < visibleRows; r++ )
+        this.appendBlankRow();
+    }
+    var s=this.options.highlightSection;
+    if (s & 1) this.attachHighlightEvents(this.tbody[0]);
+    if (s & 2) this.attachHighlightEvents(this.tbody[1]);
+  },
+
+/**
+ * @param colnum column number
+ * @return id string for a filter element
+ */
+  filterId: function(colnum) {
+    return 'RicoFilter_'+this.tableId+'_'+colnum;
+  },
+
+/**
+ * Create filter elements in heading
+ * Reads this.columns[].filterUI to determine type of filter element for each column (t=text box, s=select list, c=custom)
+ * @param r heading row where filter elements will be placed
+ */
+  createFilters: function(r) {
+    for( var c=0; c < this.headerColCnt; c++ ) {
+      var col=this.columns[c];
+      var fmt=col.format;
+      if (typeof fmt.filterUI!='string') continue;
+      var cell=this.hdrCells[r][c].cell;
+      var field,name=this.filterId(c);\r
+      var divs=cell.getElementsByTagName('div');
+      // copy text alignment from data cell
+      var align=Rico.getStyle(this.cell(0,c),'textAlign');
+      divs[1].style.textAlign=align;
+      switch (fmt.filterUI.charAt(0)) {
+        case 't':
+          // text field
+          field=Rico.createFormField(divs[1],'input',Rico.inputtypes.search ? 'search' : 'text',name,'RicoFilter');
+          var size=fmt.filterUI.match(/\d+/);
+          field.maxLength=fmt.Length || 50;\r
+          field.size=size ? parseInt(size,10) : 10;
+          if (field.type != 'search') divs[1].appendChild(Rico.clearButton(Rico.eventHandle(col,'filterClear')));
+          if (col.filterType==Rico.ColumnConst.USERFILTER && col.filterOp=='LIKE') {
+            var v=col.filterValues[0];
+            if (v.charAt(0)=='*') v=v.substr(1);
+            if (v.slice(-1)=='*') v=v.slice(0,-1);
+            field.value=v;
+            col.lastKeyFilter=v;
+          }
+          Rico.eventBind(field,'keyup',Rico.eventHandle(col,'filterKeypress'),false);
+          Rico.eventBind(field,'change',Rico.eventHandle(col,'filterKeypress'),false);
+          col.filterField=field;\r
+          break;\r
+        case 'm':
+          // multi-select
+        case 's':
+          // drop-down select
+          field=Rico.createFormField(divs[1],'select',null,name,'RicoFilter');\r
+          Rico.addSelectOption(field,this.options.FilterAllToken,Rico.getPhraseById("filterAll"));\r
+          col.filterField=field;
+          var options={};\r
+          Rico.extend(options, this.buffer.ajaxOptions);
+          var colnum=typeof(fmt.filterCol)=='number' ? fmt.filterCol : c;
+          options.parameters = this.buffer.formQueryHashXML(0,-1);
+          options.parameters.distinct = colnum;
+          options.onComplete = this.filterValuesUpdateFunc(c);
+          new Rico.ajaxRequest(this.buffer.dataSource, options);
+          break;\r
+        case 'n':
+          field=Rico.createFormField(divs[1],'select',null,name,'RicoFilter');
+          Rico.addSelectOption(field,this.options.FilterAllToken,Rico.getPhraseById("filterAll"));
+          col.filterField=field;
+          var choices=fmt.filterUI.length == 1 ? "-0+" : fmt.filterUI.substr(1);
+          if (choices.indexOf("-") >= 0) Rico.addSelectOption(field,"LT0","< 0");
+          if (choices.indexOf("0") >= 0) Rico.addSelectOption(field,"EQ0","= 0");
+          if (choices.indexOf("+") >= 0) Rico.addSelectOption(field,"GT0","> 0");
+          Rico.eventBind(col.filterField,'change',Rico.eventHandle(col,'nFilterChange'));
+          break;
+        case 'c':
+          // custom
+          if (typeof col._createFilters == 'function')
+            col._createFilters(divs[1], name);
+          break;
+      }
+    }
+    this.initFilterImage(r);
+  },
+  
+  filterValuesUpdateFunc: function(colnum) {
+    var self=this;
+    return function (request) { self.filterValuesUpdate(colnum,request); };
+  },
+
+/**
+ * update select list filter with values in AJAX response
+ * @returns true on success
+ */
+  filterValuesUpdate: function(colnum,request) {
+    var response = request.responseXML.getElementsByTagName("ajax-response");
+    Rico.log("filterValuesUpdate: "+request.status);
+    if (response == null || response.length != 1) return false;
+    response=response[0];
+    var error = response.getElementsByTagName('error');
+    if (error.length > 0) {
+      Rico.log("Data provider returned an error:\n"+Rico.getContentAsString(error[0],this.buffer.isEncoded));
+      alert(Rico.getPhraseById("requestError",Rico.getContentAsString(error[0],this.buffer.isEncoded)));
+      return false;
+    }\r
+    response=response.getElementsByTagName('response')[0];\r
+    var rowsElement = response.getElementsByTagName('rows')[0];\r
+    var col=this.columns[parseInt(colnum,10)];
+    var rows = this.buffer.dom2jstable(rowsElement);\r
+    var found = !col.filterValues || !col.filterValues.length;
+    if (!found) for (var i=0; i<rows.length; i++) {
+       if (rows[i][0] == col.filterValues[0]) {
+           found = true;
+           break;
+       }
+    }
+    if (!found) {
+       col.setUnfiltered(true);
+       if (this.options.filterHandler)
+           this.options.filterHandler();
+    }
+    var c,opt,v;
+    if (col.filterType==Rico.ColumnConst.USERFILTER && col.filterOp=='EQ') v=col.filterValues[0];
+    Rico.log('filterValuesUpdate: col='+colnum+' rows='+rows.length);
+    switch (col.format.filterUI.charAt(0)) {
+      case 'm':
+        // multi-select
+        col.mFilter = document.body.appendChild(document.createElement("div"));
+        col.mFilter.className = 'ricoLG_mFilter'
+        Rico.hide(col.mFilter);
+        var contentDiv = col.mFilter.appendChild(document.createElement("div"));
+        contentDiv.className = 'ricoLG_mFilter_content'
+        var buttonDiv = col.mFilter.appendChild(document.createElement("div"));
+        buttonDiv.className = 'ricoLG_mFilter_button'
+        col.mFilterButton=buttonDiv.appendChild(document.createElement("button"));
+        col.mFilterButton.innerHTML=Rico.getPhraseById("apply");
+        var eventName=Rico.isWebKit ? 'mousedown' : 'click';
+        Rico.eventBind(col.filterField,eventName,Rico.eventHandle(col,'mFilterSelectClick'));
+        Rico.eventBind(col.mFilterButton,'click',Rico.eventHandle(col,'mFilterFinish'));
+        //col.filterField.options[0].text=$('AllLabel').innerHTML;
+        tab = contentDiv.appendChild(document.createElement("table"));
+        tab.border=0;
+        tab.cellPadding=2;
+        tab.cellSpacing=0;
+        //tbody=(tab.tBodies.length==0) ? tab.appendChild(document.createElement("tbody")) : tab.tBodies[0];
+        var baseId=this.filterId(colnum)+'_';
+        this.createMFilterItem(tab,this.options.FilterAllToken,Rico.getPhraseById("filterAll"),baseId+'all',Rico.eventHandle(col,'mFilterAllClick'));
+        var handle=Rico.eventHandle(col,'mFilterOtherClick')
+        for (var i=0; i<rows.length; i++) {
+          if (rows[i].length>0) {
+            c=rows[i][0];
+            this.createMFilterItem(tab,c,c || Rico.getPhraseById("filterBlank"),baseId+i,handle);
+          }
+        }
+        col.mFilterInputs=contentDiv.getElementsByTagName('input');
+        col.mFilterLabels=contentDiv.getElementsByTagName('label');
+        col.mFilterFocus=col.mFilterInputs.length ? col.mFilterInputs[0] : col.mFilterButton;
+        break;
+
+      case 's':
+        // drop-down select
+        for (var i=0; i<rows.length; i++) {
+          if (rows[i].length>0) {
+            c=rows[i][0];
+           ctrans=c;
+           if (col._getdesc) ctrans = col._getdesc(c);
+           opt=Rico.addSelectOption(col.filterField,c,ctrans || Rico.getPhraseById("filterBlank"));
+            if (col.filterType==Rico.ColumnConst.USERFILTER && c==v) opt.selected=true;
+          }
+        }
+        Rico.eventBind(col.filterField,'change',Rico.eventHandle(col,'filterChange'));
+        break;
+    }
+    return true;\r
+  },
+  
+  createMFilterItem: function(table,code,description,id,eventHandle) {
+    var tr=table.insertRow(-1);
+    tr.vAlign='top';
+    if (tr.rowIndex % 2 == 1) tr.className='ricoLG_mFilter_oddrow';
+    var td1=tr.insertCell(-1)
+    var td2=tr.insertCell(-1)
+    var field=Rico.createFormField(td1,'input','checkbox',id);
+    field.value=code;
+    field.checked=true;
+    var label = td2.appendChild(document.createElement("label"));
+    label.htmlFor = id;
+    label.innerHTML=description;
+    Rico.eventBind(field,'click',eventHandle);
+  },
+
+  unplugHighlightEvents: function() {
+    var s=this.options.highlightSection;
+    if (s & 1) this.detachHighlightEvents(this.tbody[0]);
+    if (s & 2) this.detachHighlightEvents(this.tbody[1]);
+  },
+
+/**
+ * place panel names on first row of grid header (used by LiveGridForms)
+ */
+  insertPanelNames: function(r,start,limit,cellClass) {
+    Rico.log('insertPanelNames: start='+start+' limit='+limit);
+    r.className='ricoLG_hdg';
+    var lastIdx=-1, span, newCell=null, spanIdx=0;
+    for( var c=start; c < limit; c++ ) {
+      if (lastIdx == this.options.columnSpecs[c].ColGroupIdx) {
+        span++;
+      } else {
+        if (newCell) newCell.colSpan=span;
+        newCell = r.insertCell(-1);
+        if (cellClass) newCell.className=cellClass;
+        span=1;
+        lastIdx=this.options.columnSpecs[c].ColGroupIdx;
+        newCell.innerHTML=this.options.ColGroups[lastIdx];
+      }
+    }
+    if (newCell) newCell.colSpan=span;
+  },
+
+/**
+ * create grid header for table i (if none was provided)
+ */
+  createHdr: function(i,start,limit) {
+    Rico.log('createHdr: i='+i+' start='+start+' limit='+limit);
+    var mainRow = this.thead[i].insertRow(-1);
+    mainRow.id=this.tableId+'_tab'+i+'h_main';
+    mainRow.className='ricoLG_hdg';
+    for( var c=start; c < limit; c++ ) {
+      var newCell = mainRow.insertCell(-1);
+      newCell.innerHTML=this.options.columnSpecs[c].Hdg;
+    }
+  },
+
+/**
+ * move header cells in original table to grid
+ */
+  loadHdrSrc: function(hdrSrc) {
+    var i,h,c,r,newrow,cells;
+    Rico.log('loadHdrSrc start');
+    for (i=0; i<2; i++) {
+      for (r=0; r<hdrSrc.length; r++) {
+        newrow = this.thead[i].insertRow(-1);
+        newrow.className='ricoLG_hdg '+this.tableId+'_hdg'+r;
+      }
+    }
+    if (hdrSrc.length==1) {
+      cells=hdrSrc[0].cells;
+      for (c=0; cells.length > 0; c++)
+        this.thead[c<this.options.frozenColumns ? 0 : 1].rows[0].appendChild(cells[0]);
+    } else {
+      for (r=0; r<hdrSrc.length; r++) {
+        cells=hdrSrc[r].cells;
+        for (c=0,h=0; cells.length > 0; c++) {
+          if (Rico.hasClass(cells[0],'ricoFrozen')) {
+            if (r==this.headerRowIdx) this.options.frozenColumns=c+1;
+          } else {
+            h=1;
+          }
+          this.thead[h].rows[r].appendChild(cells[0]);
+        }
+      }
+    }
+    Rico.log('loadHdrSrc end');
+  },
+
+/**
+ * Size div elements
+ */
+  sizeDivs: function() {
+    Rico.log('sizeDivs: '+this.tableId);
+    //this.cancelMenu();
+    this.unhighlight();
+    this.baseSizeDivs();
+    var firstVisible=this.firstVisible();
+    if (this.pageSize == 0 || firstVisible < 0) return;
+    var totRowHt=this.columns[firstVisible].dataColDiv.offsetHeight;
+    this.rowHeight = Math.round(totRowHt/this.pageSize);
+    var scrHt=this.dataHt;
+    if (this.scrTabWi0 == this.scrTabWi) {
+      // no scrolling columns - horizontal scroll bar not needed
+      this.innerDiv.style.height=(this.hdrHt+1)+'px';
+      this.scrollDiv.style.overflowX='hidden';
+    } else {
+      this.scrollDiv.style.overflowX='scroll';
+      scrHt+=this.options.scrollBarWidth;
+    }
+    this.scrollDiv.style.height=scrHt+'px';
+    this.innerDiv.style.width=(this.scrWi)+'px';
+    this.scrollTab.style.width=(this.scrWi-this.options.scrollBarWidth)+'px';
+    //this.resizeDiv.style.height=this.frozenTabs.style.height=this.innerDiv.style.height=(this.hdrHt+this.dataHt+1)+'px';
+    this.resizeDiv.style.height=(this.hdrHt+this.dataHt+1)+'px';
+    Rico.log('sizeDivs scrHt='+scrHt+' innerHt='+this.innerDiv.style.height+' rowHt='+this.rowHeight+' pageSize='+this.pageSize);
+    var pad=(this.scrWi-this.scrTabWi < this.options.scrollBarWidth) ? 2 : 0;
+    this.shadowDiv.style.width=(this.scrTabWi+pad)+'px';
+    this.outerDiv.style.height=(this.hdrHt+scrHt)+'px';
+    this.setHorizontalScroll();
+  },
+
+  setHorizontalScroll: function() {
+    var newLeft=(-this.scrollDiv.scrollLeft)+'px';
+    this.tabs[1].style.marginLeft=newLeft;
+    this.tabs[2].style.marginLeft=newLeft;
+  },
+
+  remainingHt: function() {
+    var tabHt=this.outerDiv.offsetHeight;
+    var winHt=Rico.windowHeight();
+    var margin=Rico.isIE ? 15 : 10;
+    // if there is a horizontal scrollbar take it into account
+    if (!Rico.isIE && window.frameElement && window.frameElement.scrolling=='yes' && this.sizeTo!='parent') margin+=this.options.scrollBarWidth;
+    switch (this.sizeTo) {
+      case 'window':
+        var divTop=Rico.cumulativeOffset(this.outerDiv).top;
+        Rico.log("remainingHt/window, winHt="+winHt+' tabHt='+tabHt+' gridY='+divTop);
+        return winHt-divTop-tabHt-margin;  // allow for scrollbar and some margin
+      case 'parent':
+        var offset=this.offsetFromParent(this.outerDiv);
+        if (Rico.isIE) Rico.hide(this.outerDiv);
+        var parentHt=this.outerDiv.parentNode.clientHeight;
+        if (Rico.isIE) Rico.show(this.outerDiv);
+        Rico.log("remainingHt/parent, parentHt="+parentHt+' offset='+offset+' tabHt='+tabHt);
+        return parentHt-tabHt-offset-margin;
+      case 'data':
+      case 'body':
+        var bodyHt=Rico.isIE ? document.body.scrollHeight : (document.body.offsetHeight - 50);
+        //alert("remainingHt\n document.height="+document.height+"\n body.offsetHeight="+document.body.offsetHeight+"\n body.scrollHeight="+document.body.scrollHeight+"\n documentElement.scrollHeight="+document.documentElement.scrollHeight);
+        var remHt=winHt-bodyHt-margin;
+        if (!Rico.isWebKit) remHt-=this.options.scrollBarWidth;
+        Rico.log("remainingHt, winHt="+winHt+' pageHt='+bodyHt+' remHt='+remHt);
+        return remHt;
+      default:
+        Rico.log("remainingHt, winHt="+winHt+' tabHt='+tabHt);
+        if (this.sizeTo.slice(-1)=='%') winHt*=parseFloat(this.sizeTo)/100.0;
+        else if (this.sizeTo.slice(-2)=='px') winHt=parseInt(this.sizeTo,10);
+        return winHt-tabHt-margin;  // allow for scrollbar and some margin
+    }
+  },
+
+  offsetFromParent: function(element) {
+    var valueT = 0;
+    var elParent=element.parentNode;
+    do {
+      //Rico.log("offsetFromParent: "+element.tagName+' id='+element.id+' otop='+element.offsetTop);
+      valueT += element.offsetTop  || 0;
+      element = element.offsetParent;
+      if (!element || element==null) break;
+      var p = Rico.getStyle(element, 'position');
+      if (element.tagName=='BODY' || element.tagName=='HTML' || p=='absolute') return valueT-elParent.offsetTop;
+    } while (element != elParent);
+    return valueT;
+  },
+
+  adjustPageSize: function() {
+    Rico.log('adjustPageSize start');
+    var remHt=this.remainingHt();
+    Rico.log('adjustPageSize remHt='+remHt+' lastRow='+this.lastRowPos);
+    if (remHt > this.rowHeight)
+      this.autoAppendRows(remHt);
+    else if (remHt < 0 || this.sizeTo=='data')
+      this.autoRemoveRows(-remHt);
+    Rico.log('adjustPageSize end');
+  },
+  
+  setPageSize: function(newRowCount) {
+    Rico.log('setPageSize '+this.tableId+' newRowCount='+newRowCount);
+    newRowCount=Math.min(newRowCount,this.options.maxPageRows);
+    newRowCount=Math.max(newRowCount,this.options.minPageRows);
+    this.sizeTo='fixed';
+    var oldSize=this.pageSize;
+    while (this.pageSize > newRowCount) {
+      this.removeRow();
+    }
+    while (this.pageSize < newRowCount) {
+      this.appendBlankRow();
+    }
+    this.finishResize(oldSize);
+  },
+
+  pluginWindowResize: function() {
+    Rico.log("pluginWindowResize");
+    this.resizeWindowHandler=Rico.eventHandle(this,'resizeWindow');
+    Rico.eventBind(window, "resize", this.resizeWindowHandler, false);
+  },
+
+  unplugWindowResize: function() {
+    if (!this.resizeWindowHandler) return;
+    Rico.eventUnbind(window,"resize", this.resizeWindowHandler, false);
+    this.resizeWindowHandler=null;
+  },
+
+  resizeWindow: function() {
+    Rico.log('resizeWindow '+this.tableId+' lastRow='+this.lastRowPos+' resizeState='+this.resizeState);
+    if (this.resizeState=='finish') {
+      Rico.log('resizeWindow postponed');
+      this.resizeState='resize';
+      return;
+    }
+    if (!this.sizeTo || this.sizeTo=='fixed') {
+      this.sizeDivs();
+      return;
+    }
+    if (this.sizeTo=='parent' && Rico.getStyle(this.outerDiv.parentNode,'display') == 'none') return;
+    Rico.log('resizeWindow: about to adjustPageSize')
+    var oldSize=this.pageSize;
+    this.adjustPageSize();
+    this.finishResize(oldSize);
+  },
+
+  finishResize: function(oldSize) {
+    Rico.log('finishResize '+this.tableId);
+    if (this.pageSize > oldSize && this.buffer.totalRows>0) {
+      this.isPartialBlank=true;
+      var adjStart=this.adjustRow(this.lastRowPos);
+      this.buffer.fetch(adjStart);
+    } else if (this.pageSize < oldSize) {
+      if (this.options.onRefreshComplete) this.options.onRefreshComplete(this.contentStartPos,this.contentStartPos+this.pageSize-1);  // update bookmark
+    }
+    this.resizeState='finish';
+    Rico.runLater(20,this,'finishResize2');
+    Rico.log('Resize '+this.tableId+' complete. old size='+oldSize+' new size='+this.pageSize);
+  },
+
+  finishResize2: function() {
+    Rico.log('finishResize2 '+this.tableId+': resizeState='+this.resizeState);
+    this.sizeDivs();
+    this.updateHeightDiv();
+    if (this.resizeState=='resize') {
+      this.resizeWindow();
+    } else {
+      this.resizeState='';
+    }
+  },
+
+  topOfLastPage: function() {
+    return Math.max(this.buffer.totalRows-this.pageSize,0);
+  },
+
+  updateHeightDiv: function() {
+    var notdisp=this.topOfLastPage();
+    var ht = notdisp ? this.scrollDiv.clientHeight + Math.floor(this.rowHeight * (notdisp + 0.4)) : 1;
+    Rico.log("updateHeightDiv, ht="+ht+' scrollDiv.clientHeight='+this.scrollDiv.clientHeight+' rowsNotDisplayed='+notdisp);
+    this.shadowDiv.style.height=ht+'px';
+  },
+
+  autoRemoveRows: function(overage) {
+    if (!this.rowHeight) return;
+    var removeCnt=Math.ceil(overage / this.rowHeight);
+    if (this.sizeTo=='data')
+      removeCnt=Math.max(removeCnt,this.pageSize-this.buffer.totalRows);
+    Rico.log("autoRemoveRows overage="+overage+" removeCnt="+removeCnt);
+    for (var i=0; i<removeCnt; i++)
+      this.removeRow();
+  },
+
+  removeRow: function() {
+    if (this.pageSize <= this.options.minPageRows) return;
+    this.pageSize--;
+    for( var c=0; c < this.headerColCnt; c++ ) {
+      var cell=this.columns[c].cell(this.pageSize);
+      this.columns[c].dataColDiv.removeChild(cell);
+    }
+  },
+
+  autoAppendRows: function(overage) {
+    if (!this.rowHeight) return;
+    var addCnt=Math.floor(overage / this.rowHeight);
+    Rico.log("autoAppendRows overage="+overage+" cnt="+addCnt+" rowHt="+this.rowHeight);
+    for (var i=0; i<addCnt; i++) {
+      if (this.sizeTo=='data' && this.pageSize>=this.buffer.totalRows) break;
+      this.appendBlankRow();
+    }
+  },
+
+/**
+ * on older systems, this can be fairly slow
+ */
+  appendBlankRow: function() {
+    if (this.pageSize >= this.options.maxPageRows) return;
+    Rico.log("appendBlankRow #"+this.pageSize);
+    var cls=this.defaultRowClass(this.pageSize);
+    for( var c=0; c < this.headerColCnt; c++ ) {
+      var newdiv = document.createElement("div");
+      newdiv.className = 'ricoLG_cell '+cls;
+      newdiv.id=this.tableId+'_'+this.pageSize+'_'+c;
+      this.columns[c].dataColDiv.appendChild(newdiv);
+      if (this.columns[c]._create) {
+        this.columns[c]._create(newdiv,this.pageSize);
+      } else {
+        newdiv.innerHTML='&nbsp;';   // this seems to be required by IE
+      }
+      if (this.columns[c].format.canDrag && Rico.registerDraggable) {
+        Rico.registerDraggable( new Rico.LiveGridDraggable(this, this.pageSize, c), this.options.dndMgrIdx );
+      }
+    }
+    this.pageSize++;
+  },
+
+  defaultRowClass: function(rownum) {
+    var cls
+    if (rownum % 2==0) {
+      cls='ricoLG_evenRow';
+      //if (Rico.theme.primary) cls+=' '+Rico.theme.primary;
+    } else {
+      cls='ricoLG_oddRow';
+      //if (Rico.theme.secondary) cls+=' '+Rico.theme.secondary;
+    }
+    return cls;
+  },
+
+  handleMenuClick: function(e) {
+    if (!this.menu) return;
+    this.cancelMenu();
+    this.unhighlight(); // in case highlighting was invoked externally
+    var idx;
+    var cell=Rico.eventElement(e);
+    if (cell.className=='ricoLG_highlightDiv') {
+      idx=this.highlightIdx;
+    } else {
+      cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+      if (!cell) return;
+      idx=this.winCellIndex(cell);
+      if ((this.options.highlightSection & (idx.tabIdx+1))==0) return;
+    }
+    this.highlight(idx);
+    this.highlightEnabled=false;
+    if (this.hideScroll) this.scrollDiv.style.overflow="hidden";
+    this.menuIdx=idx;
+    if (!this.menu.div) this.menu.createDiv();
+    this.menu.liveGrid=this;
+    if (this.menu.buildGridMenu) {
+      var showMenu=this.menu.buildGridMenu(idx.row, idx.column, idx.tabIdx);
+      if (!showMenu) return;
+    }
+    if (this.options.highlightElem=='selection' && !this.isSelected(idx.cell)) {
+      this.selectCell(idx.cell);
+    }
+    var self=this;
+    this.menu.showmenu(e,function() { self.closeMenu(); });
+    return false;
+  },
+
+  closeMenu: function() {
+    if (!this.menuIdx) return;
+    if (this.hideScroll) this.scrollDiv.style.overflow="";
+    //this.unhighlight();
+    this.highlightEnabled=true;
+    this.menuIdx=null;
+  },
+
+/**
+ * @return index of cell within the window
+ */
+  winCellIndex: function(cell) {
+    var l=cell.id.lastIndexOf('_',cell.id.length);
+    var l2=cell.id.lastIndexOf('_',l-1)+1;
+    var c=parseInt(cell.id.substr(l+1));
+    var r=parseInt(cell.id.substr(l2,l));
+    return {row:r, column:c, tabIdx:this.columns[c].tabIdx, cell:cell};
+  },
+
+/**
+ * @return index of cell within the dataset
+ */
+  datasetIndex: function(cell) {
+    var idx=this.winCellIndex(cell);
+    idx.row+=this.buffer.windowPos;
+    idx.onBlankRow=(idx.row >= this.buffer.endPos());
+    return idx;
+  },
+
+  attachHighlightEvents: function(tBody) {
+    switch (this.options.highlightElem) {
+      case 'selection':
+        Rico.eventBind(tBody,"mousedown", this.options.mouseDownHandler, false);
+        /** @ignore */
+        tBody.ondrag = function () { return false; };
+        /** @ignore */
+        tBody.onselectstart = function () { return false; };
+        break;
+      case 'cursorRow':
+      case 'cursorCell':
+        Rico.eventBind(tBody,"mouseover", this.options.rowOverHandler, false);
+        break;
+    }
+  },
+
+  detachHighlightEvents: function(tBody) {
+    switch (this.options.highlightElem) {
+      case 'selection':
+        Rico.eventUnbind(tBody,"mousedown", this.options.mouseDownHandler, false);
+        tBody.ondrag = null;
+        tBody.onselectstart = null;
+        break;
+      case 'cursorRow':
+      case 'cursorCell':
+        Rico.eventUnbind(tBody,"mouseover", this.options.rowOverHandler, false);
+        break;
+    }
+  },
+
+/**
+ * @return array of objects containing row/col indexes (index values are relative to the start of the window)
+ */
+  getVisibleSelection: function() {
+    var cellList=[];
+    if (this.SelectIdxStart && this.SelectIdxEnd) {
+      var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row)-this.buffer.startPos,this.buffer.windowStart);
+      var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row)-this.buffer.startPos,this.buffer.windowEnd-1);
+      var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+      var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+      //Rico.log("getVisibleSelection "+r1+','+c1+' to '+r2+','+c2+' ('+this.SelectIdxStart.row+',startPos='+this.buffer.startPos+',windowPos='+this.buffer.windowPos+',windowEnd='+this.buffer.windowEnd+')');
+      for (var r=r1; r<=r2; r++) {
+        for (var c=c1; c<=c2; c++)
+          cellList.push({row:r-this.buffer.windowStart,column:c});
+      }
+    }
+    if (this.SelectCtrl) {
+      for (var i=0; i<this.SelectCtrl.length; i++) {
+        if (this.SelectCtrl[i].row>=this.buffer.windowStart && this.SelectCtrl[i].row<this.buffer.windowEnd)
+          cellList.push({row:this.SelectCtrl[i].row-this.buffer.windowStart,column:this.SelectCtrl[i].column});
+      }
+    }
+    return cellList;
+  },
+
+  updateSelectOutline: function() {
+    if (!this.SelectIdxStart || !this.SelectIdxEnd) return;
+    var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowStart);
+    var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowEnd-1);
+    if (r1 > r2) {
+      this.HideSelection();
+      return;
+    }
+    var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+    var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+    var top1=this.columns[c1].cell(r1-this.buffer.windowStart).offsetTop;
+    var cell2=this.columns[c1].cell(r2-this.buffer.windowStart);
+    var bottom2=cell2.offsetTop+cell2.offsetHeight;
+    var left1=this.columns[c1].dataCell.offsetLeft;
+    var left2=this.columns[c2].dataCell.offsetLeft;
+    var right2=left2+this.columns[c2].dataCell.offsetWidth;
+    //window.status='updateSelectOutline: '+r1+' '+r2+' top='+top1+' bot='+bottom2;
+    this.highlightDiv[0].style.top=this.highlightDiv[3].style.top=this.highlightDiv[1].style.top=(this.hdrHt+top1-1) + 'px';
+    this.highlightDiv[2].style.top=(this.hdrHt+bottom2-1)+'px';
+    this.highlightDiv[3].style.left=(left1-2)+'px';
+    this.highlightDiv[0].style.left=this.highlightDiv[2].style.left=(left1-1)+'px';
+    this.highlightDiv[1].style.left=(right2-1)+'px';
+    this.highlightDiv[0].style.width=this.highlightDiv[2].style.width=(right2-left1-1) + 'px';
+    this.highlightDiv[1].style.height=this.highlightDiv[3].style.height=(bottom2-top1) + 'px';
+    //this.highlightDiv[0].style.right=this.highlightDiv[2].style.right=this.highlightDiv[1].style.right=()+'px';
+    //this.highlightDiv[2].style.bottom=this.highlightDiv[3].style.bottom=this.highlightDiv[1].style.bottom=(this.hdrHt+bottom2) + 'px';
+    for (var i=0; i<4; i++)
+      this.highlightDiv[i].style.display='';
+  },
+
+  HideSelection: function() {
+    var i;
+    if (this.options.highlightMethod!='class') {
+      for (i=0; i<this.highlightDiv.length; i++)
+        this.highlightDiv[i].style.display='none';
+    }
+    if (this.options.highlightMethod!='outline') {
+      var cellList=this.getVisibleSelection();
+      Rico.log("HideSelection "+cellList.length);
+      for (i=0; i<cellList.length; i++)
+        this.unhighlightCell(this.columns[cellList[i].column].cell(cellList[i].row));
+    }
+  },
+
+  ShowSelection: function() {
+    if (this.options.highlightMethod!='class')
+      this.updateSelectOutline();
+    if (this.options.highlightMethod!='outline') {
+      var cellList=this.getVisibleSelection();
+      for (var i=0; i<cellList.length; i++)
+        this.highlightCell(this.columns[cellList[i].column].cell(cellList[i].row));
+    }
+  },
+
+  ClearSelection: function() {
+    Rico.log("ClearSelection");
+    this.HideSelection();
+    this.SelectIdxStart=null;
+    this.SelectIdxEnd=null;
+    this.SelectCtrl=[];
+  },
+
+  selectCell: function(cell) {
+    this.ClearSelection();
+    this.SelectIdxStart=this.SelectIdxEnd=this.datasetIndex(cell);
+    this.ShowSelection();
+  },
+
+  AdjustSelection: function(cell) {
+    var newIdx=this.datasetIndex(cell);
+    if (this.SelectIdxStart.tabIdx != newIdx.tabIdx) return;
+    this.HideSelection();
+    this.SelectIdxEnd=newIdx;
+    this.ShowSelection();
+  },
+
+  RefreshSelection: function() {
+    var cellList=this.getVisibleSelection();
+    for (var i=0; i<cellList.length; i++) {
+      this.columns[cellList[i].column].displayValue(cellList[i].row);
+    }
+  },
+
+  FillSelection: function(newVal,newStyle) {
+    if (this.SelectIdxStart && this.SelectIdxEnd) {
+      var r1=Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row);
+      var r2=Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row);
+      var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+      var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+      for (var r=r1; r<=r2; r++) {
+        for (var c=c1; c<=c2; c++) {
+          this.buffer.setValue(r,c,newVal,newStyle);
+        }
+      }
+    }
+    if (this.SelectCtrl) {
+      for (var i=0; i<this.SelectCtrl.length; i++) {
+        this.buffer.setValue(this.SelectCtrl[i].row,this.SelectCtrl[i].column,newVal,newStyle);
+      }
+    }
+    this.RefreshSelection();
+  },
+
+/**
+ * Process mouse down event
+ * @param e event object
+ */
+  selectMouseDown: function(e) {
+    if (this.highlightEnabled==false) return true;
+    this.cancelMenu();
+    var cell=Rico.eventElement(e);
+    if (!Rico.eventLeftClick(e)) return true;
+    cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+    if (!cell) return true;
+    Rico.eventStop(e);
+    var newIdx=this.datasetIndex(cell);
+    if (newIdx.onBlankRow) return true;
+    Rico.log("selectMouseDown @"+newIdx.row+','+newIdx.column);
+    if (e.ctrlKey) {
+      if (!this.SelectIdxStart || this.options.highlightMethod!='class') return true;
+      if (!this.isSelected(cell)) {
+        this.highlightCell(cell);
+        this.SelectCtrl.push(this.datasetIndex(cell));
+      } else {
+        for (var i=0; i<this.SelectCtrl.length; i++) {
+          if (this.SelectCtrl[i].row==newIdx.row && this.SelectCtrl[i].column==newIdx.column) {
+            this.unhighlightCell(cell);
+            this.SelectCtrl.splice(i,1);
+            break;
+          }
+        }
+      }
+    } else if (e.shiftKey) {
+      if (!this.SelectIdxStart) return true;
+      this.AdjustSelection(cell);
+    } else {
+      this.selectCell(cell);
+      this.pluginSelect();
+    }
+    return false;
+  },
+
+  pluginSelect: function() {
+    if (this.selectPluggedIn) return;
+    var tBody=this.tbody[this.SelectIdxStart.tabIdx];
+    Rico.eventBind(tBody,"mouseover", this.options.mouseOverHandler, false);
+    Rico.eventBind(this.outerDiv,"mouseup",  this.options.mouseUpHandler,  false);
+    this.selectPluggedIn=true;
+  },
+
+  unplugSelect: function() {
+    if (!this.selectPluggedIn) return;
+    var tBody=this.tbody[this.SelectIdxStart.tabIdx];
+    Rico.eventUnbind(tBody,"mouseover", this.options.mouseOverHandler , false);
+    Rico.eventUnbind(this.outerDiv,"mouseup", this.options.mouseUpHandler , false);
+    this.selectPluggedIn=false;
+  },
+
+  selectMouseUp: function(e) {
+    this.unplugSelect();
+    var cell=Rico.eventElement(e);
+    cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+    if (!cell) return;
+    if (this.SelectIdxStart && this.SelectIdxEnd)
+      this.AdjustSelection(cell);
+    else
+      this.ClearSelection();
+  },
+
+  selectMouseOver: function(e) {
+    var cell=Rico.eventElement(e);
+    cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+    if (!cell) return;
+    this.AdjustSelection(cell);
+    Rico.eventStop(e);
+  },
+
+  isSelected: function(cell) {
+    if (this.options.highlightMethod!='outline') return Rico.hasClass(cell,this.options.highlightClass);
+    if (!this.SelectIdxStart || !this.SelectIdxEnd) return false;
+    var r1=Math.max(Math.min(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowStart);
+    var r2=Math.min(Math.max(this.SelectIdxEnd.row,this.SelectIdxStart.row), this.buffer.windowEnd-1);
+    if (r1 > r2) return false;
+    var c1=Math.min(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+    var c2=Math.max(this.SelectIdxEnd.column,this.SelectIdxStart.column);
+    var curIdx=this.datasetIndex(cell);
+    return (r1<=curIdx.row && curIdx.row<=r2 && c1<=curIdx.column && curIdx.column<=c2);
+  },
+
+  highlightCell: function(cell) {
+    Rico.addClass(cell,this.options.highlightClass);
+  },
+
+  unhighlightCell: function(cell) {
+    if (cell) Rico.removeClass(cell,this.options.highlightClass);
+  },
+
+  selectRow: function(r) {
+    for (var c=0; c<this.columns.length; c++)
+      this.highlightCell(this.columns[c].cell(r));
+  },
+
+  unselectRow: function(r) {
+    for (var c=0; c<this.columns.length; c++)
+      this.unhighlightCell(this.columns[c].cell(r));
+  },
+
+  rowMouseOver: function(e) {
+    if (!this.highlightEnabled) return;
+    var cell=Rico.eventElement(e);
+    cell=Rico.getParentByTagName(cell,'div','ricoLG_cell');
+    if (!cell) return;
+    var newIdx=this.winCellIndex(cell);
+    if ((this.options.highlightSection & (newIdx.tabIdx+1))==0) return;
+    this.highlight(newIdx);
+  },
+
+  highlight: function(newIdx) {
+    if (this.options.highlightMethod!='outline') this.cursorSetClass(newIdx);
+    if (this.options.highlightMethod!='class') this.cursorOutline(newIdx);
+    this.highlightIdx=newIdx;
+  },
+
+  cursorSetClass: function(newIdx) {
+    switch (this.options.highlightElem) {
+      case 'menuCell':
+      case 'cursorCell':
+        if (this.highlightIdx) this.unhighlightCell(this.highlightIdx.cell);
+        this.highlightCell(newIdx.cell);
+        break;
+      case 'menuRow':
+      case 'cursorRow':
+        if (this.highlightIdx) this.unselectRow(this.highlightIdx.row);
+        var s1=this.options.highlightSection & 1;
+        var s2=this.options.highlightSection & 2;
+        var c0=s1 ? 0 : this.options.frozenColumns;
+        var c1=s2 ? this.columns.length : this.options.frozenColumns;
+        for (var c=c0; c<c1; c++)
+          this.highlightCell(this.columns[c].cell(newIdx.row));
+        break;
+      default: return;
+    }
+  },
+
+  cursorOutline: function(newIdx) {
+    var div;
+    switch (this.options.highlightElem) {
+      case 'menuCell':
+      case 'cursorCell':
+        div=this.highlightDiv[newIdx.tabIdx];
+        div.style.left=(this.columns[newIdx.column].dataCell.offsetLeft-1)+'px';
+        div.style.width=this.columns[newIdx.column].colWidth;
+        this.highlightDiv[1-newIdx.tabIdx].style.display='none';
+        break;
+      case 'menuRow':
+      case 'cursorRow':
+        div=this.highlightDiv[0];
+        var s1=this.options.highlightSection & 1;
+        var s2=this.options.highlightSection & 2;
+        div.style.left=s1 ? '0px' : this.frozenTabs.style.width;
+        div.style.width=((s1 ? this.frozenTabs.offsetWidth : 0) + (s2 ? this.innerDiv.offsetWidth : 0) - 4)+'px';
+        break;
+      default: return;
+    }
+    div.style.top=(this.hdrHt+newIdx.row*this.rowHeight-1)+'px';
+    div.style.height=(this.rowHeight-1)+'px';
+    div.style.display='';
+  },
+
+  unhighlight: function() {
+    switch (this.options.highlightElem) {
+      case 'menuCell':
+        //this.highlightIdx=this.menuIdx;
+        /*jsl:fallthru*/
+      case 'cursorCell':
+        if (this.highlightIdx) this.unhighlightCell(this.highlightIdx.cell);
+        if (!this.highlightDiv) return;
+        for (var i=0; i<2; i++)
+          this.highlightDiv[i].style.display='none';
+        break;
+      case 'menuRow':
+        //this.highlightIdx=this.menuIdx;
+        /*jsl:fallthru*/
+      case 'cursorRow':
+        if (this.highlightIdx) this.unselectRow(this.highlightIdx.row);
+        if (this.highlightDiv) this.highlightDiv[0].style.display='none';
+        break;
+    }
+  },
+
+  resetContents: function() {
+    Rico.log("resetContents");
+    this.ClearSelection();
+    this.buffer.clear();
+    this.clearRows();
+    this.clearBookmark();
+  },
+
+  setImages: function() {
+    for (var n=0; n<this.columns.length; n++)
+      this.columns[n].setImage();
+  },
+
+/**
+ * @return column index, or -1 if there are no sorted columns
+ */
+  findSortedColumn: function() {
+    for (var n=0; n<this.columns.length; n++) {
+      if (this.columns[n].isSorted()) return n;
+    }
+    return -1;
+  },
+
+/**
+ * Searches options.columnSpecs colAttr for matching colValue
+ * @return array of matching column indexes
+ */
+  findColumnsBySpec: function(colAttr, colValue) {
+    var result=[];
+    for (var n=0; n<this.options.columnSpecs.length; n++) {
+      if (this.options.columnSpecs[n][colAttr] == colValue) result.push(n);
+    }
+    return result;
+  },
+
+/**
+ * Set initial sort
+ */
+  setSortUI: function( columnIdOrNum, sortDirection ) {
+    Rico.log("setSortUI: "+columnIdOrNum+' '+sortDirection);
+    var colnum=this.findSortedColumn();
+    if (colnum >= 0) {
+      sortDirection=this.columns[colnum].getSortDirection();
+    } else {
+      if (typeof sortDirection!='string') {
+        sortDirection=Rico.ColumnConst.SORT_ASC;
+      } else {
+        sortDirection=sortDirection.toUpperCase();
+        if (sortDirection != Rico.ColumnConst.SORT_DESC) sortDirection=Rico.ColumnConst.SORT_ASC;
+      }
+      switch (typeof columnIdOrNum) {
+        case 'string':
+          colnum=this.findColumnsBySpec('id',columnIdOrNum);
+          break;
+        case 'number':
+          colnum=columnIdOrNum;
+          break;
+      }
+    }
+    if (typeof(colnum)!='number' || colnum < 0) return;
+    this.clearSort();
+    this.columns[colnum].setSorted(sortDirection);
+    this.buffer.sortBuffer(colnum);
+  },
+
+/**
+ * clear sort flag on all columns
+ */
+  clearSort: function() {
+    for (var x=0;x<this.columns.length;x++)
+      this.columns[x].setUnsorted();
+  },
+
+/**
+ * clear filters on all columns
+ */
+  clearFilters: function() {
+    for (var x=0;x<this.columns.length;x++) {
+      this.columns[x].setUnfiltered(true);
+    }
+    if (this.options.filterHandler) {
+      this.options.filterHandler();
+    }
+  },
+
+/**
+ * returns number of columns with a user filter set
+ */
+  filterCount: function() {
+    for (var x=0,cnt=0;x<this.columns.length;x++) {
+      if (this.columns[x].isFiltered()) cnt++;
+    }
+    return cnt;
+  },
+
+  sortHandler: function() {
+    this.cancelMenu();
+    this.ClearSelection();
+    this.setImages();
+    var n=this.findSortedColumn();
+    if (n < 0) return;
+    Rico.log("sortHandler: sorting column "+n);
+    this.buffer.sortBuffer(n);
+    this.clearRows();
+    this.scrollDiv.scrollTop = 0;
+    this.buffer.fetch(0);
+  },
+
+  filterHandler: function() {
+    Rico.log("filterHandler");
+    this.cancelMenu();
+    if (this.buffer.processingRequest) {
+      this.queueFilter=true;
+      return;
+    }
+    this.unplugScroll();
+    this.ClearSelection();
+    this.setImages();
+    this.clearBookmark();
+    this.clearRows();
+    this.buffer.fetch(-1);
+    Rico.runLater(10,this,'pluginScroll'); // resetting ht div can cause a scroll event, triggering an extra fetch
+  },
+
+  clearBookmark: function() {
+    if (this.bookmark) this.bookmark.innerHTML="&nbsp;";
+  },
+
+  bookmarkHandler: function(firstrow,lastrow) {
+    var newhtml;
+    if (isNaN(firstrow) || !this.bookmark) return;
+    var totrows=this.buffer.totalRows;
+    if (totrows < lastrow) lastrow=totrows;
+    if (totrows<=0) {
+      newhtml = Rico.getPhraseById('bookmarkNoMatch');
+    } else if (lastrow<0) {
+      newhtml = Rico.getPhraseById('bookmarkNoRec');
+    } else if (this.buffer.foundRowCount) {
+      newhtml = Rico.getPhraseById('bookmarkExact',firstrow,lastrow,totrows);
+    } else {
+      newhtml = Rico.getPhraseById('bookmarkAbout',firstrow,lastrow,totrows);
+    }
+    this.bookmark.innerHTML = newhtml;
+  },
+
+  clearRows: function() {
+    if (this.isBlank==true) return;
+    for (var c=0; c < this.columns.length; c++)
+      this.columns[c].clearColumn();
+    this.isBlank = true;
+  },
+
+  refreshContents: function(startPos) {
+    Rico.log("refreshContents1 "+this.tableId+": startPos="+startPos+" lastRow="+this.lastRowPos+" PartBlank="+this.isPartialBlank+" pageSize="+this.pageSize);
+    this.hideMsg();
+    this.cancelMenu();
+    this.unhighlight(); // in case highlighting was manually invoked
+    if (this.queueFilter) {
+      Rico.log("refreshContents: cancelling refresh because filter has changed");
+      this.queueFilter=false;
+      this.filterHandler();
+      return;
+    }
+    this.highlightEnabled=this.options.highlightSection!='none';
+    var viewPrecedesBuffer = this.buffer.startPos > startPos;
+    var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos;
+    this.contentStartPos = contentStartPos+1;
+    var contentEndPos = Math.min(this.buffer.startPos + this.buffer.size, startPos + this.pageSize);
+    this.buffer.setWindow(contentStartPos, contentEndPos);
+    Rico.log('refreshContents2 '+this.tableId+': cStartPos='+contentStartPos+' cEndPos='+contentEndPos+' vPrecedesBuf='+viewPrecedesBuffer+' b.startPos='+this.buffer.startPos);
+    if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) return;
+    this.isBlank = false;
+    var onRefreshComplete = this.options.onRefreshComplete;
+
+    if ((startPos + this.pageSize < this.buffer.startPos) ||
+        (this.buffer.startPos + this.buffer.size < startPos) ||
+        (this.buffer.size == 0)) {
+      this.clearRows();
+      if (onRefreshComplete) onRefreshComplete(this.contentStartPos,contentEndPos);  // update bookmark
+      return;
+    }
+
+    Rico.log('refreshContents: contentStartPos='+contentStartPos+' contentEndPos='+contentEndPos+' viewPrecedesBuffer='+viewPrecedesBuffer);
+    var rowSize = contentEndPos - contentStartPos;
+    var blankSize = this.pageSize - rowSize;
+    var blankOffset = viewPrecedesBuffer ? 0: rowSize;
+    var contentOffset = viewPrecedesBuffer ? blankSize: 0;
+
+    for (var r=0; r < rowSize; r++) { //initialize what we have
+      for (var c=0; c < this.columns.length; c++)
+        this.columns[c].displayValue(r + contentOffset);
+    }
+    for (var i=0; i < blankSize; i++)     // blank out the rest
+      this.blankRow(i + blankOffset);
+    if (this.options.highlightElem=='selection') this.ShowSelection();
+    this.isPartialBlank = blankSize > 0;
+    this.lastRowPos = startPos;
+    Rico.log("refreshContents complete, startPos="+startPos);
+    if (onRefreshComplete) onRefreshComplete(this.contentStartPos,contentEndPos);  // update bookmark
+  },
+
+  scrollToRow: function(rowOffset) {
+     var p=this.rowToPixel(rowOffset);
+     Rico.log("scrollToRow, rowOffset="+rowOffset+" pixel="+p);
+     this.scrollDiv.scrollTop = p; // this causes a scroll event
+     if ( this.options.onscroll )
+        this.options.onscroll( this, rowOffset );
+  },
+
+  scrollUp: function() {
+     this.moveRelative(-1);
+  },
+
+  scrollDown: function() {
+     this.moveRelative(1);
+  },
+
+  pageUp: function() {
+     this.moveRelative(-this.pageSize);
+  },
+
+  pageDown: function() {
+     this.moveRelative(this.pageSize);
+  },
+
+  adjustRow: function(rowOffset) {
+     var notdisp=this.topOfLastPage();
+     if (notdisp == 0 || !rowOffset) return 0;
+     return Math.min(notdisp,rowOffset);
+  },
+
+  rowToPixel: function(rowOffset) {
+     return this.adjustRow(rowOffset) * this.rowHeight;
+  },
+
+/**
+ * @returns row to display at top of scroll div
+ */
+  pixeltorow: function(p) {
+     var notdisp=this.topOfLastPage();
+     if (notdisp == 0) return 0;
+     var prow=parseInt(p/this.rowHeight,10);
+     return Math.min(notdisp,prow);
+  },
+
+  moveRelative: function(relOffset) {
+     var newoffset=Math.max(this.scrollDiv.scrollTop+relOffset*this.rowHeight,0);
+     newoffset=Math.min(newoffset,this.scrollDiv.scrollHeight);
+     //Rico.log("moveRelative, newoffset="+newoffset);
+     this.scrollDiv.scrollTop=newoffset;
+  },
+
+  pluginScroll: function() {
+     if (this.scrollPluggedIn) return;
+     Rico.log("pluginScroll: wheelEvent="+this.wheelEvent);
+     Rico.eventBind(this.scrollDiv,"scroll",this.scrollEventFunc, false);
+     for (var t=0; t<3; t++)
+       Rico.eventBind(this.tabs[t],this.wheelEvent,this.wheelEventFunc, false);
+     this.scrollPluggedIn=true;
+  },
+
+  unplugScroll: function() {
+     if (!this.scrollPluggedIn) return;
+     Rico.log("unplugScroll");
+     Rico.eventUnbind(this.scrollDiv,"scroll", this.scrollEventFunc , false);
+     for (var t=0; t<2; t++)
+       Rico.eventUnbind(this.tabs[t],this.wheelEvent,this.wheelEventFunc, false);
+     this.scrollPluggedIn=false;
+  },
+
+  handleWheel: function(e) {
+    var delta = 1;
+    if (e.wheelDelta) {
+      if (Rico.isOpera)
+        delta = e.wheelDelta/120;
+      else if (Rico.isWebKit)
+        delta = -e.wheelDelta/12;
+      else
+        delta = -e.wheelDelta/120;
+    } else if (e.detail) {
+      delta = e.detail/3; /* Mozilla/Gecko */
+    }
+    if (delta) this.moveRelative(delta);
+    Rico.eventStop(e);
+    return false;
+  },
+
+  handleScroll: function(e) {
+     if ( this.scrollTimeout )
+       clearTimeout( this.scrollTimeout );
+     this.setHorizontalScroll();
+     var scrtop=this.scrollDiv.scrollTop;
+     var vscrollDiff = this.lastScrollPos-scrtop;
+     if (vscrollDiff == 0.00) return;
+     var newrow=this.pixeltorow(scrtop);
+     if (newrow == this.lastRowPos && !this.isPartialBlank && !this.isBlank) return;
+     var stamp1 = new Date();
+     Rico.log("handleScroll, newrow="+newrow+" scrtop="+scrtop);
+     if (this.options.highlightElem=='selection') this.HideSelection();
+     this.buffer.fetch(newrow);
+     if (this.options.onscroll) this.options.onscroll(this, newrow);
+     this.scrollTimeout = Rico.runLater(1200,this,'scrollIdle');
+     this.lastScrollPos = this.scrollDiv.scrollTop;
+     var stamp2 = new Date();
+     //Rico.log("handleScroll, time="+(stamp2.getTime()-stamp1.getTime()));
+  },
+
+  scrollIdle: function() {
+     if ( this.options.onscrollidle )
+        this.options.onscrollidle();
+  }
+
+};
+
+
+Rico.LiveGridColumn = function(grid,colIdx,hdrInfo,tabIdx) {
+  this.initialize(grid,colIdx,hdrInfo,tabIdx);
+};
+
+Rico.LiveGridColumn.prototype = 
+/** @lends Rico.LiveGridColumn# */
+{
+/**
+ * Implements a LiveGrid column. Also contains static properties used by SimpleGrid columns.
+ * @extends Rico.TableColumnBase
+ * @constructs
+ */
+initialize: function(liveGrid,colIdx,hdrInfo,tabIdx) {
+  Rico.extend(this, new Rico.TableColumnBase());
+  this.baseInit(liveGrid,colIdx,hdrInfo,tabIdx);
+  this.buffer=liveGrid.buffer;
+  if (typeof(this.format.type)!='string' || this.format.EntryType=='tinyMCE') this.format.type='html';
+  if (typeof this.isNullable!='boolean') this.isNullable = /number|date/.test(this.format.type);
+  this.isText = /html|text/.test(this.format.type);
+  Rico.log(" sortable="+this.sortable+" filterable="+this.filterable+" hideable="+this.hideable+" isNullable="+this.isNullable+' isText='+this.isText);
+  this.fixHeaders(this.liveGrid.tableId, this.options.hdrIconsFirst);
+  if (this['format_'+this.format.type]) {
+    this._format=this['format_'+this.format.type];
+  }
+  if (this.format.control) {
+    // copy all properties/methods that start with '_'
+    if (typeof this.format.control=='string') {
+      this.format.control=eval(this.format.control);
+    }
+    for (var property in this.format.control) {
+      if (property.charAt(0)=='_') {
+        Rico.log("Copying control property "+property+ ' to ' + this);
+        this[property] = this.format.control[property];
+      }
+    }
+  }
+},
+
+/**
+ * Sorts the column in ascending order
+ */
+sortAsc: function() {
+  this.setColumnSort(Rico.ColumnConst.SORT_ASC);
+},
+
+/**
+ * Sorts the column in descending order
+ */
+sortDesc: function() {
+  this.setColumnSort(Rico.ColumnConst.SORT_DESC);
+},
+
+/**
+ * Sorts the column in the specified direction
+ * @param direction must be one of Rico.ColumnConst.UNSORTED, .SORT_ASC, or .SORT_DESC
+ */
+setColumnSort: function(direction) {
+  this.liveGrid.clearSort();
+  this.setSorted(direction);
+  if (this.liveGrid.options.saveColumnInfo.sort)
+    this.liveGrid.setCookie();
+  if (this.options.sortHandler)
+    this.options.sortHandler();
+},
+
+/**
+ * @returns true if this column is allowed to be sorted
+ */
+isSortable: function() {
+  return this.sortable;
+},
+
+/**
+ * @returns true if this column is currently sorted
+ */
+isSorted: function() {
+  return this.currentSort != Rico.ColumnConst.UNSORTED;
+},
+
+/**
+ * @returns Rico.ColumnConst.UNSORTED, .SORT_ASC, or .SORT_DESC
+ */
+getSortDirection: function() {
+  return this.currentSort;
+},
+
+/**
+ * toggle the sort sequence for this column
+ */
+toggleSort: function() {
+  if (this.buffer && this.buffer.totalRows==0) return;
+  if (this.currentSort == Rico.ColumnConst.SORT_ASC)
+    this.sortDesc();
+  else
+    this.sortAsc();
+},
+
+/**
+ * Flags that this column is not sorted
+ */
+setUnsorted: function() {
+  this.setSorted(Rico.ColumnConst.UNSORTED);
+},
+
+/**
+ * Flags that this column is sorted, but doesn't actually carry out the sort
+ * @param direction must be one of Rico.ColumnConst.UNSORTED, .SORT_ASC, or .SORT_DESC
+ */
+setSorted: function(direction) {
+  this.currentSort = direction;
+},
+
+/**
+ * @returns true if this column is allowed to be filtered
+ */
+canFilter: function() {
+  return this.filterable;
+},
+
+/**
+ * @returns a textual representation of how this column is filtered
+ */
+getFilterText: function() {
+  var vals=[];
+  for (var i=0; i<this.filterValues.length; i++) {
+    var v=this.filterValues[i];
+    vals.push(v=='' ? Rico.getPhraseById('filterBlank') : v);
+  }
+  switch (this.filterOp) {
+    case 'EQ':   return '= '+vals.join(', ');
+    case 'NE':   return Rico.getPhraseById('filterNot',vals.join(', '));
+    case 'LT':   return '< '+vals[0];
+    case 'GT':   return '> '+vals[0];
+    case 'LE':   return '<= '+vals[0];
+    case 'GE':   return '>= '+vals[0];
+    case 'LIKE': return Rico.getPhraseById('filterLike',vals[0]);
+    case 'NULL': return Rico.getPhraseById('filterEmpty');
+    case 'NOTNULL': return Rico.getPhraseById('filterNotEmpty');
+  }
+  return '?';
+},
+
+/**
+ * @returns returns the query string representation of the filter
+ */
+getFilterQueryParm: function() {
+  if (this.filterType == Rico.ColumnConst.UNFILTERED) return '';
+  var retval='&f['+this.index+'][op]='+this.filterOp;
+  retval+='&f['+this.index+'][len]='+this.filterValues.length;
+  for (var i=0; i<this.filterValues.length; i++) {
+    retval+='&f['+this.index+']['+i+']='+escape(this.filterValues[i]);
+  }
+  return retval;
+},
+
+/**
+ * removes the filter from this column
+ */
+setUnfiltered: function(skipHandler) {
+  this.filterType = Rico.ColumnConst.UNFILTERED;
+  if (this.liveGrid.options.saveColumnInfo.filter)
+    this.liveGrid.setCookie();
+  if (this.removeFilterFunc)
+    this.removeFilterFunc();
+  if (this.options.filterHandler && !skipHandler)
+    this.options.filterHandler();
+},
+
+setFilterEQ: function() {
+  this.setUserFilter('EQ');
+},
+setFilterNE: function() {
+  this.setUserFilter('NE');
+},
+addFilterNE: function() {
+  this.filterValues.push(this.userFilter);
+  if (this.liveGrid.options.saveColumnInfo.filter)
+    this.liveGrid.setCookie();
+  if (this.options.filterHandler)
+    this.options.filterHandler();
+},
+setFilterGE: function() { this.setUserFilter('GE'); },
+setFilterLE: function() { this.setUserFilter('LE'); },
+setFilterKW: function(keyword) {
+  if (keyword!='' && keyword!=null) {
+    this.setFilter('LIKE',keyword,Rico.ColumnConst.USERFILTER);
+  } else {
+    this.setUnfiltered(false);
+  }
+},
+
+setUserFilter: function(relop) {
+  this.setFilter(relop,this.userFilter,Rico.ColumnConst.USERFILTER);
+},
+
+setSystemFilter: function(relop,filter) {
+  this.setFilter(relop,filter,Rico.ColumnConst.SYSTEMFILTER);
+},
+
+setFilter: function(relop,filter,type,removeFilterFunc) {
+  this.filterValues = typeof(filter)=='object' ? filter : [filter];
+  this.filterType = type;
+  this.filterOp = relop;
+  if (type == Rico.ColumnConst.USERFILTER && this.liveGrid.options.saveColumnInfo.filter)
+    this.liveGrid.setCookie();
+  this.removeFilterFunc=removeFilterFunc;
+  if (this.options.filterHandler)
+    this.options.filterHandler();
+},
+
+isFiltered: function() {
+  return this.filterType == Rico.ColumnConst.USERFILTER;
+},
+
+filterChange: function(e) {
+  var selbox=Rico.eventElement(e);
+  if (selbox.value==this.liveGrid.options.FilterAllToken)
+    this.setUnfiltered();
+  else
+    this.setFilter('EQ',selbox.value,Rico.ColumnConst.USERFILTER,function() {selbox.selectedIndex=0;});
+},
+
+nFilterChange: function(e) {
+  var selbox=Rico.eventElement(e);
+  if (selbox.value==this.liveGrid.options.FilterAllToken) {
+    this.setUnfiltered();
+  } else {
+    var op=selbox.value.substr(0,2);
+    var value=selbox.value.substr(2);
+    this.setFilter(op,value,Rico.ColumnConst.USERFILTER,function() {selbox.selectedIndex=0;});
+  }
+},
+
+filterClear: function(e) {\r
+  this.filterField.value='';
+  this.setUnfiltered();\r
+},
+
+filterKeypress: function(e) {\r
+  var txtbox=Rico.eventElement(e);
+  if (typeof this.lastKeyFilter != 'string') this.lastKeyFilter='';\r
+  if (this.lastKeyFilter==txtbox.value) return;\r
+  var v=txtbox.value;\r
+  Rico.log("filterKeypress: "+this.index+' '+v);\r
+  this.lastKeyFilter=v;
+  if (v=='' || v=='*')\r
+    this.setUnfiltered();\r
+  else {
+    this.setFilter('LIKE', v, Rico.ColumnConst.USERFILTER, function() {txtbox.value='';});
+  }\r
+},\r
+
+mFilterSelectClick: function(e) {
+  Rico.eventStop(e);
+  if (this.mFilter.style.display!='none') {
+    this.mFilterFinish(e);
+    if (Rico.isIE && Rico.ieVersion <= 6) {
+      this.filterField.focus();
+    } else {
+      this.filterField.blur();
+    }
+  } else {
+    var offset=Rico.cumulativeOffset(this.filterField);
+    this.mFilter.style.top=(offset.top+this.filterField.offsetHeight)+'px';
+    this.mFilter.style.left=offset.left+'px';
+    this.mFilter.style.width=Math.min(this.filterField.offsetWidth,parseInt(this.colWidth,10))+'px';
+    Rico.show(this.mFilter);
+    this.mFilterFocus.focus();
+  }
+},
+
+mFilterFinish: function(e) {
+  if (!this.mFilterChange) {
+    Rico.hide(this.mFilter);
+    return;
+  }
+  if (this.mFilterInputs[0].checked) {
+    this.mFilterReset();
+    Rico.hide(this.mFilter);
+    this.setUnfiltered();
+    return;
+  }
+  var newValues=[];
+  var newLabels=[];
+  for (var i=1; i<this.mFilterInputs.length; i++) {
+    if (this.mFilterInputs[i].checked) {
+      newValues.push(this.mFilterInputs[i].value)
+      newLabels.push(this.mFilterLabels[i].innerHTML)
+    }
+  }
+  if (newValues.length > 0) {
+    var newText=newLabels.join(', ');
+    this.filterField.options[0].text=newText;
+    this.filterField.title=newText;
+    Rico.hide(this.mFilter);
+    this.mFilterChange=false;
+    var self=this;
+    this.setFilter('EQ',newValues,Rico.ColumnConst.USERFILTER,function() { self.mFilterReset(); });
+  } else {
+    alert('Please select at least one value');
+  }
+},
+
+mFilterReset: function() {
+  var newText=this.mFilterLabels[0].innerHTML;  // all
+  this.filterField.options[0].text=newText;
+  this.filterField.title=newText;
+},
+
+mFilterAllClick: function(e) {
+  var allChecked=this.mFilterInputs[0].checked;
+  for (var i=1; i<this.mFilterInputs.length; i++) {
+    this.mFilterInputs[i].checked=allChecked;
+  }
+  this.mFilterChange=true;
+},
+
+mFilterOtherClick: function(e) {
+  this.mFilterInputs[0].checked=false;
+  this.mFilterChange=true;
+},
+
+format_text: function(v) {
+  if (typeof v!='string')
+    return '&nbsp;';
+  else
+    return v.replace(/&/g, '&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+},
+
+format_number: function(v) {
+  if (typeof v=='undefined' || v=='' || v==null)
+    return '&nbsp;';
+  else
+    return Rico.formatNumber(v,this.format);
+},
+
+format_datetime: function(v) {
+  if (typeof v=='undefined' || v=='' || v==null)
+    return '&nbsp;';
+  else {
+    var d=Rico.setISO8601(v);
+    if (!d) return v;
+    return (this.format.prefix || '')+Rico.formatDate(d,this.format.dateFmt || 'translateDateTime')+(this.format.suffix || '');
+  }
+},
+
+// converts GMT/UTC to local time
+format_utcaslocaltime: function(v) {
+  if (typeof v=='undefined' || v=='' || v==null)
+    return '&nbsp;';
+  else {
+    var tz=new Date();
+    var d=Rico.setISO8601(v,-tz.getTimezoneOffset());
+    if (!d) return v;
+    return (this.format.prefix || '')+Rico.formatDate(d,this.format.dateFmt || 'translateDateTime')+(this.format.suffix || '');
+  }
+},
+
+format_date: function(v) {
+  if (typeof v=='undefined' || v==null || v=='')
+    return '&nbsp;';
+  else {
+    var d=Rico.setISO8601(v);
+    if (!d) return v;
+    return (this.format.prefix || '')+Rico.formatDate(d,this.format.dateFmt || 'translateDate')+(this.format.suffix || '');
+  }
+},
+
+fixHeaders: function(prefix, iconsfirst) {
+  if (this.sortable) {
+    var handler=Rico.eventHandle(this,'toggleSort');
+    switch (this.options.headingSort) {
+      case 'link':
+        var a=Rico.wrapChildren(this.hdrCellDiv,'ricoSort',undefined,'a');
+        a.href = "javascript:void(0)";
+        Rico.eventBind(a,"click", handler);
+        break;
+      case 'hover':
+        Rico.eventBind(this.hdrCellDiv,"click", handler);
+        break;
+    }
+  }
+  this.imgFilter = document.createElement('span');
+  this.imgFilter.style.display='none';
+  this.imgFilter.className='rico-icon ricoLG_filterCol';
+  this.imgSort = document.createElement('span');
+  this.imgSort.style.display='none';
+  this.imgSort.style.verticalAlign='top';
+  if (iconsfirst) {
+    this.hdrCellDiv.insertBefore(this.imgSort,this.hdrCellDiv.firstChild);
+    this.hdrCellDiv.insertBefore(this.imgFilter,this.hdrCellDiv.firstChild);
+  } else {
+    this.hdrCellDiv.appendChild(this.imgFilter);
+    this.hdrCellDiv.appendChild(this.imgSort);
+  }
+  if (!this.format.filterUI) {
+    Rico.eventBind(this.imgFilter, 'click', Rico.eventHandle(this,'filterClick'), false);
+  }
+},
+
+filterClick: function(e) {
+  if (this.filterType==Rico.ColumnConst.USERFILTER && this.filterOp=='LIKE') {
+    this.liveGrid.openKeyword(this.index);
+  }
+},
+
+getValue: function(windowRow) {
+  return this.buffer.getWindowCell(windowRow,this.index);
+},
+
+getBufferStyle: function(windowRow) {
+  return this.buffer.getWindowStyle(windowRow,this.index);
+},
+
+setValue: function(windowRow,newval) {
+  this.buffer.setWindowValue(windowRow,this.index,newval);
+},
+
+_format: function(v) {
+  return v;
+},
+
+_display: function(v,gridCell) {
+  gridCell.innerHTML=this._format(v);
+},
+
+_export: function(v) {
+  return this._format(v);
+},
+
+exportBuffer: function(bufRow) {
+  return this._export(this.buffer.getValue(bufRow,this.index));
+},
+
+displayValue: function(windowRow) {
+  var bufval=this.getValue(windowRow);
+  if (bufval==null) {
+    this.clearCell(windowRow);
+    return;
+  }
+  var gridCell=this.cell(windowRow);
+  this._display(bufval,gridCell,windowRow);
+  if (this.buffer.options.acceptStyle) {
+    gridCell.style.cssText=this.getBufferStyle(windowRow);
+  }
+}
+
+};
diff --git a/lib/rico3/minsrc/ricoLiveGridAjax.js b/lib/rico3/minsrc/ricoLiveGridAjax.js
new file mode 100644 (file)
index 0000000..2276b1d
--- /dev/null
@@ -0,0 +1,616 @@
+/*
+ *  (c) 2005-2011 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+if(typeof Rico=='undefined') throw("LiveGridAjax requires the Rico JavaScript framework");
+
+if (!Rico.Buffer) Rico.Buffer = {};
+
+Rico.Buffer.AjaxLoadOnce = function(url,options,ajaxOptions) {
+  this.initialize(url,options,ajaxOptions);
+}
+
+Rico.Buffer.AjaxLoadOnce.prototype = {
+/**
+ * @class Implements buffer for LiveGrid. Loads data from server via a single AJAX call.
+ * @extends Rico.Buffer.Base
+ * @constructs
+ */
+  initialize: function(url,options,ajaxOptions) {
+    Rico.extend(this, new Rico.Buffer.Base());
+    Rico.extend(this, Rico.Buffer.AjaxXMLMethods);
+    this.dataSource=url;
+    this.options.bufferTimeout=20000;            // time to wait for ajax response (milliseconds)
+    this.options.requestParameters=[];
+    this.options.waitMsg=Rico.getPhraseById("waitForData");  // replace this with an image tag if you prefer
+    this.options.canFilter=true;
+    this.options.fmt='xml';
+    Rico.extend(this.options, options || {});
+    this.ajaxOptions = { parameters: null, method : 'get' };
+    Rico.extend(this.ajaxOptions, ajaxOptions || {});
+    this.requestCount=0;
+    this.processingRequest=false;
+    this.pendingRequest=-2;
+    this.fetchData=true;
+    this.sortParm={};
+  }
+}
+
+Rico.Buffer.AjaxXMLMethods = {
+
+/** @lends Rico.Buffer.AjaxLoadOnce# */
+  fetch: function(offset) {
+    if (this.fetchData) {
+      this.foundRowCount=true;
+      this.fetchData=false;
+      this.processingRequest=true;
+      this.liveGrid.showMsg(this.options.waitMsg);
+      this.timeoutHandler = Rico.runLater(this.options.bufferTimeout,this,'handleTimedOut');
+      this.ajaxOptions.parameters = this.formQueryHashXML(0,-1);
+      Rico.log('sending request');
+      var self=this;
+      if (typeof this.dataSource=='string') {
+        this.ajaxOptions.onComplete = function(xhr) { self.ajaxUpdate(offset,xhr); };
+        new Rico.ajaxRequest(this.dataSource, this.ajaxOptions);
+      } else {
+        this.ajaxOptions.onComplete = function(newRows, newStyle, totalRows, errMsg) { self.jsUpdate(offset, newRows, newStyle, totalRows, errMsg); };
+        this.dataSource(this.ajaxOptions);
+      }
+    } else {
+      if (offset < 0) {
+        this.applyFilters();
+        this.setTotalRows(this.size);
+        offset=0;
+      }
+      this.liveGrid.refreshContents(offset);
+    }
+  },
+
+/**
+ * Server did not respond in time... assume that there could have been
+ * an error, and allow requests to be processed again.
+ */
+  handleTimedOut: function() {
+    Rico.log("Request Timed Out");
+    this.liveGrid.showMsg(Rico.getPhraseById("requestTimedOut"));
+  },
+
+  formQueryHashXML: function(startPos,fetchSize) {
+    var queryHash= {
+      id: this.liveGrid.tableId,
+      page_size: (typeof fetchSize=='number') ? fetchSize : this.totalRows,
+      offset: startPos.toString()
+    };
+    queryHash[this.liveGrid.actionId]="query";
+    if (this.options.requestParameters) {
+      for ( var i=0; i < this.options.requestParameters.length; i++ ) {
+        var anArg = this.options.requestParameters[i];
+        if ( anArg.name != undefined && anArg.value != undefined ) {
+          queryHash[anArg.name]=anArg.value;
+        } else {
+          var ePos  = anArg.indexOf('=');
+          var argName  = anArg.substring( 0, ePos );
+          var argValue = anArg.substring( ePos + 1 );
+          queryHash[argName]=argValue;
+        }
+      }
+    }
+    return queryHash;
+  },
+
+  clearTimer: function() {
+    if(typeof this.timeoutHandler != "number") return;
+    window.clearTimeout(this.timeoutHandler);
+    delete this.timeoutHandler;
+  },
+
+  // used by both LoadOnce and SQL buffers
+  jsUpdate: function(startPos, newRows, newStyle, totalRows, errMsg) {
+    this.clearTimer();
+    this.processingRequest=false;
+    Rico.log("jsUpdate: "+arguments.length);
+    if (errMsg) {
+      Rico.log("jsUpdate: received error="+errMsg);
+      this.liveGrid.showMsg(Rico.getPhraseById("requestError",errMsg));
+      return;
+    }
+    this.rcvdRows = newRows.length;
+    if (typeof totalRows=='number') {
+      this.rowcntContent = totalRows.toString();
+      this.rcvdRowCount = true;
+      this.foundRowCount = true;
+      Rico.log("jsUpdate: found RowCount="+this.rowcntContent);
+    }
+    this.updateBuffer(startPos, newRows, newStyle);
+    if (this.options.onAjaxUpdate)
+      this.options.onAjaxUpdate();
+    this.updateGrid(startPos);
+    if (this.options.TimeOut && this.timerMsg)
+      this.restartSessionTimer();
+    if (this.pendingRequest>=-1) {
+      var offset=this.pendingRequest;
+      Rico.log("jsUpdate: found pending request for offset="+offset);
+      this.pendingRequest=-2;
+      this.fetch(offset);
+    }
+  },
+
+  // used by both LoadOnce and SQL buffers
+  ajaxUpdate: function(startPos,xhr) {
+    this.clearTimer();
+    this.processingRequest=false;
+    if (xhr.status != 200) {
+      Rico.log("ajaxUpdate: received http error="+xhr.status);
+      this.liveGrid.showMsg(Rico.getPhraseById("httpError",xhr.status));
+      return;
+    }
+    Rico.log("ajaxUpdate: startPos="+startPos);
+    this._responseHandler=this['processResponse'+this.options.fmt.toUpperCase()];
+    if (!this._responseHandler(startPos,xhr)) return;
+    if (this.options.onAjaxUpdate)
+      this.options.onAjaxUpdate();
+    this.updateGrid(startPos);
+    if (this.options.TimeOut && this.timerMsg)
+      this.restartSessionTimer();
+    if (this.pendingRequest>=-1) {
+      var offset=this.pendingRequest;
+      Rico.log("ajaxUpdate: found pending request for offset="+offset);
+      this.pendingRequest=-2;
+      this.fetch(offset);
+    }
+  },
+  
+  // used by both LoadOnce and SQL buffers
+  processResponseXML: function(startPos,request) {
+    // The response text may contain META DATA for debugging if client side debugging is enabled in VS\r
+    var xmlDoc = request.responseXML;\r
+    if (request.responseText.substring(0, 4) == "<!--") {\r
+      var nEnd = request.responseText.indexOf("-->");\r
+      if (nEnd == -1) {\r
+        this.liveGrid.showMsg('Web server error - client side debugging may be enabled');\r
+        return false;\r
+      }\r
+      xmlDoc = Rico.createXmlDocument();\r
+      xmlDoc.loadXML(request.responseText.substring(nEnd+3));\r
+    }
+    
+    if (!xmlDoc) {
+      alert("Data provider returned an invalid XML response");
+      Rico.log("Data provider returned an invalid XML response");
+      return false;
+    }
+
+    // process children of <ajax-response>
+    var response = xmlDoc.getElementsByTagName("ajax-response");
+    if (response == null || response.length != 1) {
+      alert("Received invalid response from server");
+      return false;
+    }
+    Rico.log("Processing ajax-response");
+    this.rcvdRows = 0;
+    this.rcvdRowCount = false;
+    var ajaxResponse=response[0];
+    var debugtags = ajaxResponse.getElementsByTagName('debug');
+    for (var i=0; i<debugtags.length; i++)
+      Rico.log("ajaxUpdate: debug msg "+i+": "+Rico.getContentAsString(debugtags[i],this.options.isEncoded));
+    var error = ajaxResponse.getElementsByTagName('error');
+    if (error.length > 0) {
+      var msg=Rico.getContentAsString(error[0],this.options.isEncoded);
+      alert("Data provider returned an error:\n"+msg);
+      Rico.log("Data provider returned an error:\n"+msg);
+      return false;
+    }
+    var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
+    if (!rowsElement) {
+      Rico.log("ajaxUpdate: invalid response");
+      this.liveGrid.showMsg(Rico.getPhraseById("invalidResponse"));
+      return false;
+    }
+    var rowcnttags = ajaxResponse.getElementsByTagName('rowcount');
+    if (rowcnttags && rowcnttags.length==1) {
+      this.rowcntContent = Rico.getContentAsString(rowcnttags[0],this.options.isEncoded);
+      this.rcvdRowCount = true;
+      this.foundRowCount = true;
+      Rico.log("ajaxUpdate: found RowCount="+this.rowcntContent);
+    }
+
+    // process <rows>
+    this.updateUI = rowsElement.getAttribute("update_ui") == "true";
+    this.rcvdOffset = rowsElement.getAttribute("offset");
+    Rico.log("ajaxUpdate: rcvdOffset="+this.rcvdOffset);
+    var newRows = this.dom2jstable(rowsElement);
+    var newStyle = (this.options.acceptStyle) ? this.dom2jstableStyle(rowsElement) : false;
+    this.rcvdRows = newRows.length;
+    this.updateBuffer(startPos, newRows, newStyle);
+    return true;
+  },
+
+  dom2jstableStyle: function(rowsElement,firstRow) {
+    Rico.log("dom2jstableStyle start");
+    var newRows = [];
+    var trs = rowsElement.getElementsByTagName("tr");
+    for ( var i=firstRow || 0; i < trs.length; i++ ) {
+      var row = [];
+      var cells = trs[i].getElementsByTagName("td");
+      for ( var j=0; j < cells.length ; j++ ) {
+        row[j]=cells[j].getAttribute('style') || '';
+      }
+      newRows.push( row );
+    }
+    Rico.log("dom2jstableStyle end");
+    return newRows;
+  },
+
+  processResponseJSON: function(startPos,request) {
+    var json = Rico.getJSON(request);
+    if (!json || json == null) {
+      alert("Data provider returned an invalid JSON response");
+      Rico.log("Data provider returned an invalid JSON response");
+      return false;
+    }
+
+    if (json.debug) {
+      for (var i=0; i<json.debug.length; i++)
+        Rico.writeDebugMsg("debug msg "+i+": "+json.debug[i]);
+    }
+    if (json.error) {
+      alert("Data provider returned an error:\n"+json.error);
+      Rico.writeDebugMsg("Data provider returned an error:\n"+json.error);
+      return false;
+    }
+
+    if (json.rowcount) {
+      this.rowcntContent = json.rowcount;
+      this.rcvdRowCount = true;
+      this.foundRowCount = true;
+      Rico.writeDebugMsg("loadRows, found RowCount="+json.rowcount);
+    }
+
+    this.rcvdRows = json.rows.length;
+    this.updateBuffer(startPos, json.rows, json.styles);
+    return true;
+  },
+
+  // specific to LoadOnce buffer
+  updateBuffer: function(start, newRows, newStyle) {
+    this.baseRows = newRows;
+    this.attr = newStyle;
+    Rico.log("updateBuffer: # of rows="+this.rcvdRows);
+    this.rcvdRowCount=true;
+    this.rowcntContent=this.rcvdRows;
+    if (typeof this.delayedSortCol=='number')
+      this.sortBuffer(this.delayedSortCol);
+    this.applyFilters();
+    this.startPos = 0;
+  },
+
+  // used by both LoadOnce and SQL buffers
+  updateGrid: function(offset) {
+    Rico.log("updateGrid, size="+this.size+' rcv cnt type='+typeof(this.rowcntContent));
+    var newpos;
+    if (this.rcvdRowCount==true) {
+      Rico.log("found row cnt: "+this.rowcntContent);
+      var eofrow=parseInt(this.rowcntContent,10);
+      var lastTotalRows=this.totalRows;
+      if (!isNaN(eofrow) && eofrow!=lastTotalRows) {
+        this.setTotalRows(eofrow);
+        newpos=Math.min(this.liveGrid.topOfLastPage(),offset);
+        Rico.log("updateGrid: new rowcnt="+eofrow+" newpos="+newpos);
+        this.liveGrid.scrollToRow(newpos);
+        if ( this.isInRange(newpos) ) {
+          this.liveGrid.refreshContents(newpos);
+        } else {
+          this.fetch(newpos);
+        }
+        return;
+      }
+    } else {
+      var lastbufrow=offset+this.rcvdRows;
+      if (lastbufrow>this.totalRows) {
+        var newcnt=lastbufrow;
+        Rico.log("extending totrows to "+newcnt);
+        this.setTotalRows(newcnt);
+      }
+    }
+    newpos=this.liveGrid.pixeltorow(this.liveGrid.scrollDiv.scrollTop);
+    Rico.log("updateGrid: newpos="+newpos);
+    this.liveGrid.refreshContents(newpos);
+  }
+
+};
+
+
+
+Rico.Buffer.AjaxSQL = function(url,options,ajaxOptions) {
+  this.initialize(url,options,ajaxOptions);
+}
+
+Rico.Buffer.AjaxSQL.prototype = {
+/**
+ * @class Implements buffer for LiveGrid. Loads data from server in chunks as user scrolls through the grid.
+ * @extends Rico.Buffer.AjaxLoadOnce
+ * @constructs
+ */
+  initialize: function(url,options,ajaxOptions) {
+    Rico.extend(this, new Rico.Buffer.AjaxLoadOnce());
+    Rico.extend(this, Rico.Buffer.AjaxSQLMethods);
+    this.dataSource=url;
+    this.options.canFilter=true;
+    this.options.largeBufferSize  = 7.0;   // 7 pages
+    this.options.nearLimitFactor  = 1.0;   // 1 page
+    this.options.canRefresh=true;
+    Rico.extend(this.options, options || {});
+    Rico.extend(this.ajaxOptions, ajaxOptions || {});
+  }
+}
+
+Rico.Buffer.AjaxSQLMethods = {
+/** @lends Rico.Buffer.AjaxSQL# */
+
+  registerGrid: function(liveGrid) {
+    this.liveGrid = liveGrid;
+    this.sessionExpired=false;
+    this.timerMsg=document.getElementById(liveGrid.tableId+'_timer');
+    if (this.options.TimeOut && this.timerMsg) {
+      if (!this.timerMsg.title) this.timerMsg.title=Rico.getPhraseById("sessionExpireMinutes");
+      this.restartSessionTimer();
+    }
+  },
+
+  setBufferSize: function(pageSize) {
+    this.maxFetchSize = Math.max(50,parseInt(this.options.largeBufferSize * pageSize,10));
+    this.nearLimit = parseInt(this.options.nearLimitFactor * pageSize,10);
+    this.maxBufferSize = this.maxFetchSize * 3;
+  },
+
+  restartSessionTimer: function() {
+    if (this.sessionExpired==true) return;
+    this.sessionEndTime = (new Date()).getTime() + this.options.TimeOut*60000;
+    if (this.sessionTimer) clearTimeout(this.sessionTimer);
+    this.updateSessionTimer();
+  },
+
+  updateSessionTimer: function() {
+    var now=(new Date()).getTime();
+    if (now > this.sessionEndTime) {
+      this.displaySessionTimer(Rico.getPhraseById("sessionExpired"));
+      this.timerMsg.style.backgroundColor="red";
+      this.sessionExpired=true;
+    } else {
+      var timeRemaining=Math.ceil((this.sessionEndTime - now) / 60000);
+      this.displaySessionTimer(timeRemaining);
+      this.sessionTimer=Rico.runLater(10000,this,'updateSessionTimer');
+    }
+  },
+
+  displaySessionTimer: function(msg) {
+    this.timerMsg.innerHTML='&nbsp;'+msg+'&nbsp;';
+  },
+
+  /**
+   * Update the grid with fresh data from the database, maintaining scroll position.
+   * @param resetRowCount indicates whether the total row count should be refreshed as well
+   */
+  refresh: function(resetRowCount) {
+    var lastGridPos=this.liveGrid.lastRowPos;\r
+    this.clear();
+    if (resetRowCount) {
+      this.setTotalRows(0);
+      this.foundRowCount = false;
+    }
+    this.liveGrid.clearBookmark();
+    this.liveGrid.clearRows();
+    this.fetch(lastGridPos);
+  },
+
+  /**
+   * Fetch data from database.
+   * @param offset position (row) within the dataset (-1=clear existing buffer before issuing request)
+   */
+  fetch: function(offset) {
+    Rico.log("AjaxSQL fetch: offset="+offset+', lastOffset='+this.lastOffset);
+    if (this.processingRequest) {
+      Rico.log("AjaxSQL fetch: queue request");
+      this.pendingRequest=offset;
+      return;
+    }
+    if ((typeof offset == 'undefined') || (offset < 0)) {
+      this.clear();
+      this.setTotalRows(0);
+      this.foundRowCount = false;
+      offset=0;
+    }
+    var lastOffset = this.lastOffset;
+    this.lastOffset = offset;
+    if (this.isInRange(offset)) {
+      Rico.log("AjaxSQL fetch: in buffer");
+      this.liveGrid.refreshContents(offset);
+      if (offset > lastOffset) {
+        if (offset+this.liveGrid.pageSize < this.endPos()-this.nearLimit) return;
+        if (this.endPos()==this.totalRows && this.foundRowCount) return;
+      } else if (offset < lastOffset) {
+        if (offset > this.startPos+this.nearLimit) return;
+        if (this.startPos==0) return;
+      } else return;
+    }
+    if (offset >= this.totalRows && this.foundRowCount) return;
+
+    this.processingRequest=true;
+    Rico.log("AjaxSQL fetch: processing offset="+offset);
+    var bufferStartPos = this.getFetchOffset(offset);
+    var fetchSize = this.getFetchSize(bufferStartPos);
+    var partialLoaded = false;
+
+    this.liveGrid.showMsg(this.options.waitMsg);
+    this.timeoutHandler = Rico.runLater(this.options.bufferTimeout, this, 'handleTimedOut');
+    this.ajaxOptions.parameters = this.formQueryHashSQL(bufferStartPos,fetchSize,this.options.fmt);
+    this.requestCount++;
+    Rico.log('sending req #'+this.requestCount);
+    var self=this;
+    if (typeof this.dataSource=='string') {
+      this.ajaxOptions.onComplete = function(xhr) { self.ajaxUpdate(bufferStartPos, xhr); };
+      new Rico.ajaxRequest(this.dataSource, this.ajaxOptions);
+    } else {
+      this.ajaxOptions.onComplete = function(newRows, newStyle, totalRows, errMsg) { self.jsUpdate(bufferStartPos, newRows, newStyle, totalRows, errMsg); };
+      this.dataSource(this.ajaxOptions);
+    }
+  },
+
+  formQueryHashSQL: function(startPos,fetchSize,fmt) {
+    var queryHash=this.formQueryHashXML(startPos,fetchSize);
+    if (!this.foundRowCount) queryHash['get_total']='true';
+    if (fmt) queryHash._fmt=fmt;
+
+    // sort
+    Rico.extend(queryHash,this.sortParm);
+
+    // filters
+    for (var n=0; n<this.liveGrid.columns.length; n++) {
+      var c=this.liveGrid.columns[n];
+      if (c.filterType == Rico.ColumnConst.UNFILTERED) continue;
+      var colnum=typeof(c.format.filterCol)=='number' ? c.format.filterCol : c.index;
+      queryHash['f['+colnum+'][op]']=c.filterOp;
+      queryHash['f['+colnum+'][len]']=c.filterValues.length;
+      for (var i=0; i<c.filterValues.length; i++) {
+        var fval=c.filterValues[i];
+       if (c.format.type == 'date') {
+           var parts = fval.split('.');
+           if (parts.length > 1) {
+               parts.reverse();
+               for (var j=0; j < parts.length; j++)
+                   if (parts[j].length == 1)
+                       parts[j] = '0' + parts[j].toString();
+               fval = parts.join('-');
+           }
+       }
+        if (c.filterOp=='LIKE' && fval.indexOf('*')==-1) {
+           if (c.format.filterUI.charAt(1) == '^') fval=fval+'*';
+           else if (c.format.filterUI.charAt(1) == '$') fval='*'+fval;
+           else if (c.format.filterUI.charAt(1) == '=') queryHash['f['+colnum+'][op]']='EQ';
+           else fval='*'+fval+'*';
+       }
+        queryHash['f['+colnum+']['+i+']']=fval;
+      }
+    }
+    return queryHash;
+  },
+
+  getFetchSize: function(adjustedOffset) {
+    var adjustedSize = 0;
+    if (adjustedOffset >= this.startPos) { //appending
+      var endFetchOffset = this.maxFetchSize + adjustedOffset;
+      adjustedSize = endFetchOffset - adjustedOffset;
+      if(adjustedOffset == 0 && adjustedSize < this.maxFetchSize)
+        adjustedSize = this.maxFetchSize;
+      Rico.log("getFetchSize/append, adjustedSize="+adjustedSize+" adjustedOffset="+adjustedOffset+' endFetchOffset='+endFetchOffset);
+    } else { //prepending
+      adjustedSize = Math.min(this.startPos - adjustedOffset,this.maxFetchSize);
+    }
+    return adjustedSize;
+  },
+
+  getFetchOffset: function(offset) {
+    var adjustedOffset = offset;
+    if (offset > this.startPos)
+      adjustedOffset = Math.max(offset, this.endPos());  //appending
+    else if (offset + this.maxFetchSize >= this.startPos)
+      adjustedOffset = Math.max(this.startPos - this.maxFetchSize, 0);  //prepending
+    return adjustedOffset;
+  },
+
+  updateBuffer: function(start, newRows, newStyle) {
+    Rico.log("updateBuffer: start="+start+", # of rows="+this.rcvdRows);
+    if (this.rows.length == 0) { // initial load
+      this.rows = newRows;
+      this.attr = newStyle;
+      this.startPos = start;
+    } else if (start > this.startPos) { //appending
+      if (this.startPos + this.rows.length < start) {
+        this.rows =  newRows;
+        this.attr = newStyle;
+        this.startPos = start;
+      } else {
+        this.rows = this.rows.concat( newRows.slice(0, newRows.length));
+        if (this.attr && newStyle) this.attr = this.attr.concat( newStyle.slice(0, newStyle.length));
+        if (this.rows.length > this.maxBufferSize) {
+          var fullSize = this.rows.length;
+          this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length);
+          if (this.attr) this.attr = this.attr.slice(this.attr.length - this.maxBufferSize, this.attr.length);
+          this.startPos = this.startPos +  (fullSize - this.rows.length);
+        }
+      }
+    } else { //prepending
+      if (start + newRows.length < this.startPos) {
+        this.rows = newRows;
+        this.attr = newStyle;
+      } else {
+        this.rows = newRows.slice(0, this.startPos).concat(this.rows);
+        if (newStyle) this.attr = newStyle.slice(0, this.startPos).concat(this.attr);
+        if (this.maxBufferSize && this.rows.length > this.maxBufferSize) {
+          this.rows = this.rows.slice(0, this.maxBufferSize);
+          if (this.attr) this.attr = this.attr.slice(0, this.maxBufferSize);
+        }
+      }
+      this.startPos =  start;
+    }
+    this.size = this.rows.length;
+  },
+
+  sortBuffer: function(colnum) {
+    this.sortParm={};
+    var col=this.liveGrid.columns[colnum];
+    if (this.options.sortParmFmt) {
+      this.sortParm['sort_col']=col[this.options.sortParmFmt];
+      this.sortParm['sort_dir']=col.getSortDirection();
+    } else {
+      this.sortParm['s'+colnum]=col.getSortDirection();
+    }
+    this.clear();
+  },
+
+  printAllSQL: function(exportType) {
+    var parms=this.formQueryHashSQL(0,this.liveGrid.options.maxPrint,exportType);
+    parms.hidden=this.liveGrid.listInvisible('index').join(',');
+    var url=this.dataSource+'?'+Rico.toQueryString(parms);
+    window.open(url,'',this.liveGrid.options.exportWindow);
+  },
+
+  printVisibleSQL: function(exportType) {
+    var parms=this.formQueryHashSQL(this.liveGrid.contentStartPos-1, this.liveGrid.pageSize, exportType);
+    parms.hidden=this.liveGrid.listInvisible('index').join(',');
+    var url=this.dataSource+'?'+Rico.toQueryString(parms);
+    window.open(url,'',this.liveGrid.options.exportWindow);
+  },
+
+  // for datasource that is a javascript function
+  _printAll: function() {
+    this.liveGrid.exportStart();
+    this.ajaxOptions.parameters = this.formQueryHashSQL(0,this.liveGrid.options.maxPrint);
+    var self=this;
+    this.ajaxOptions.onComplete = function() { self._jsExport(); };
+    this.dataSource(this.ajaxOptions);
+  },
+
+  _jsExport: function(newRows, newStyle, totalRows, errMsg) {
+    Rico.log("_jsExport: "+arguments.length);
+    if (errMsg) {
+      Rico.log("_jsExport: received error="+errMsg);
+      this.liveGrid.showMsg(Rico.getPhraseById("requestError",errMsg));
+      return;
+    }
+    this.exportBuffer(newRows,0);
+    this.liveGrid.exportFinish();
+  }
+
+};
diff --git a/lib/rico3/minsrc/ricoLiveGridControls.js b/lib/rico3/minsrc/ricoLiveGridControls.js
new file mode 100644 (file)
index 0000000..1393387
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+ *  (c) 2005-2009 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+\r
+// -----------------------------------------------------\r
+//\r
+// Custom formatting for LiveGrid columns\r
+//\r
+// columnSpecs Usage: { type:'control', control:new Rico.TableColumn.CONTROLNAME() }\r
+//\r
+// -----------------------------------------------------\r
+
+Rico.TableColumn = {};
+\r
+Rico.TableColumn.checkboxKey = function(showKey) {
+  this.initialize(showKey);
+}
+
+Rico.TableColumn.checkboxKey.prototype = {
+/**
+ * @class Custom formatting for a LiveGrid column.
+ * Display unique key column as: &lt;checkbox&gt; &lt;key value&gt;
+ * and keep track of which keys the user selects
+ * Key values should not contain &lt;, &gt;, or &amp;
+ * @constructs
+ */
+  initialize: function(showKey) {
+    this._checkboxes=[];
+    this._spans=[];
+    this._KeyHash={};
+    this._showKey=showKey;\r
+  },
+
+  _create: function(gridCell,windowRow) {
+    this._checkboxes[windowRow]=Rico.createFormField(gridCell,'input','checkbox',this.liveGrid.tableId+'_chkbox_'+this.index+'_'+windowRow);
+    this._spans[windowRow]=Rico.createFormField(gridCell,'span',null,this.liveGrid.tableId+'_desc_'+this.index+'_'+windowRow);
+    this._clear(gridCell,windowRow);
+    Rico.eventBind(this._checkboxes[windowRow], 'click', Rico.eventHandle(this,'_onclick'));
+  },
+
+  _onclick: function(e) {
+    var elem=Rico.eventElement(e);
+    var windowRow=parseInt(elem.id.substr((elem.id.lastIndexOf('_',elem.id.length)+1)));  //faster than split
+    var v=this.getValue(windowRow);
+    if (elem.checked)
+      this._addChecked(v);
+    else
+      this._remChecked(v);
+  },
+
+  _clear: function(gridCell,windowRow) {
+    var box=this._checkboxes[windowRow];
+    box.checked=false;
+    box.style.display='none';
+    this._spans[windowRow].innerHTML='';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    var box=this._checkboxes[windowRow];
+    box.style.display='';
+    box.checked=this._KeyHash[v];
+    if (this._showKey) this._spans[windowRow].innerHTML=v;
+  },
+
+  _SelectedKeys: function() {
+    return Rico.keys(this._KeyHash);
+  },
+
+  _addChecked: function(k){\r
+    this._KeyHash[k]=1;\r
+  },\r
+\r
+  _remChecked: function(k){\r
+    delete this._KeyHash[k];\r
+  }\r
+}
+
+
+Rico.TableColumn.checkbox = function(checkedValue, uncheckedValue, defaultValue, readOnly)
+{
+  this.initialize(checkedValue, uncheckedValue, defaultValue, readOnly);
+}
+
+Rico.TableColumn.checkbox.prototype = {
+/**
+ * @class display checkboxes for two-valued column (e.g. yes/no)
+ * @constructs
+ */
+  initialize: function(checkedValue, uncheckedValue, defaultValue, readOnly) {
+    this._checkedValue=checkedValue;
+    this._uncheckedValue=uncheckedValue;
+    this._defaultValue=defaultValue || false;
+    this._readOnly=readOnly || false;
+    this._checkboxes=[];
+  },
+
+  _create: function(gridCell,windowRow) {
+    this._checkboxes[windowRow]=Rico.createFormField(gridCell,'input','checkbox',this.liveGrid.tableId+'_chkbox_'+this.index+'_'+windowRow);
+    this._clear(gridCell,windowRow);
+    if (this._readOnly)
+      this._checkboxes[windowRow].disabled=true;
+    else
+      Rico.eventBind(this._checkboxes[windowRow], 'click', Rico.eventHandle(this,'_onclick'));
+  },
+
+  _onclick: function(e) {
+    var elem=Rico.eventElement(e);
+    var windowRow=parseInt(elem.id.substr((elem.id.lastIndexOf('_',elem.id.length)+1)));  //faster than split
+    var newval=elem.checked ? this._checkedValue : this._uncheckedValue;
+    this.setValue(windowRow,newval);
+  },
+
+  _clear: function(gridCell,windowRow) {
+    var box=this._checkboxes[windowRow];
+    box.checked=this._defaultValue;
+    box.style.display='none';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    var box=this._checkboxes[windowRow];
+    box.style.display='';
+    box.checked=(v==this._checkedValue);
+  }
+
+}
+
+
+Rico.TableColumn.textbox = function(boxSize, boxMaxLen, readOnly) {
+  this.initialize(boxSize, boxMaxLen, readOnly);
+}
+
+Rico.TableColumn.textbox.prototype = {
+/**
+ * @class display value in a text box
+ * @constructs
+ */
+  initialize: function(boxSize, boxMaxLen, readOnly) {
+    this._boxSize=boxSize;
+    this._boxMaxLen=boxMaxLen;
+    this._readOnly=readOnly || false;
+    this._textboxes=[];
+  },
+
+  _create: function(gridCell,windowRow) {
+    var box=Rico.createFormField(gridCell,'input','text',this.liveGrid.tableId+'_txtbox_'+this.index+'_'+windowRow);
+    box.size=this._boxSize;
+    box.maxLength=this._boxMaxLen;
+    this._textboxes[windowRow]=box;
+    this._clear(gridCell,windowRow);
+    if (this._readOnly)
+      box.disabled=true;
+    else
+      Rico.eventBind(box, 'change', Rico.eventHandle(this,'_onchange'));
+  },
+
+  _onchange: function(e) {
+    var elem=Event.element(e);
+    var windowRow=parseInt(elem.id.substr((elem.id.lastIndexOf('_',elem.id.length)+1)));  //faster than split
+    this.setValue(windowRow,elem.value);
+  },
+
+  _clear: function(gridCell,windowRow) {
+    var box=this._textboxes[windowRow];
+    box.value='';
+    box.style.display='none';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    var box=this._textboxes[windowRow];
+    box.style.display='';
+    box.value=v;
+  }
+
+}
+
+
+Rico.TableColumn.bgColor = function() {
+}
+
+Rico.TableColumn.bgColor.prototype = {
+/**
+ * @class database value contains a css color name/value
+ */
+ _clear: function(gridCell,windowRow) {
+    gridCell.style.backgroundColor='';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    gridCell.style.backgroundColor=v;
+  }
+
+}
+
+
+Rico.TableColumn.link = function(href,target,linktext) {
+  this.initialize(href,target,linktext);
+}
+
+Rico.TableColumn.link.prototype = {
+/**
+ * @class database value contains a url to another page
+ * @constructs
+ */
+  initialize: function(href,target,linktext) {
+    this._href=href;
+    this._target=target;
+    this._linktext=linktext;
+    this._anchors=[];
+  },
+
+  _create: function(gridCell,windowRow) {
+    var a = gridCell.appendChild(document.createElement('a'));
+    if (this._target) a.target=this._target;
+    a.href='';
+    a.innerHTML=Rico.isIE ? '&nbsp;' : '';
+    this._anchors[windowRow] = a;
+  },
+
+  _clear: function(gridCell,windowRow) {
+    this._anchors[windowRow].style.display='none';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    var buf=this.liveGrid.buffer;
+    var href=this._href=='self' ? v : this._href.replace(/\{\d+\}/g,
+      function ($1) {
+        var colIdx=parseInt($1.substr(1),10);
+        return encodeURIComponent(buf.getWindowValue(windowRow,colIdx));
+      }
+    );
+    var desc=this._linktext || v;
+    if (href && desc) {
+      this._anchors[windowRow].href=href;
+      this._anchors[windowRow].innerHTML=desc;
+      this._anchors[windowRow].style.display=Rico.isIE ? 'inline-block' : '';
+    } else {
+      this._clear(gridCell,windowRow);
+    }
+  }
+
+};
+
+
+Rico.TableColumn.image = function(prefix,suffix) {
+  this.initialize(prefix,suffix);
+};
+
+Rico.TableColumn.image.prototype = {
+/**
+ * @class database value contains a url to an image
+ * @constructs
+ */
+  initialize: function(prefix,suffix) {
+    this._img=[];
+    this._prefix=prefix || '';
+    this._suffix=suffix || '';
+  },
+
+  _create: function(gridCell,windowRow) {
+    this._img[windowRow]=Rico.createFormField(gridCell,'img',null,this.liveGrid.tableId+'_img_'+this.index+'_'+windowRow);
+    this._clear(gridCell,windowRow);
+  },
+
+  _clear: function(gridCell,windowRow) {
+    var img=this._img[windowRow];
+    img.style.display='none';
+    img.src='';
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    var img=this._img[windowRow];
+    this._img[windowRow].src=this._prefix+v+this._suffix;
+    img.style.display='';
+  }
+
+};
+
+
+Rico.TableColumn.lookup = function(map, defaultCode, defaultDesc) {
+  this.initialize(map, defaultCode, defaultDesc);
+};
+
+Rico.TableColumn.lookup.prototype = {
+/**
+ * @class map a database value to a display value
+ * @constructs
+ */
+  initialize: function(map, defaultCode, defaultDesc) {
+    this._map=map;
+    this._defaultCode=defaultCode || '';
+    this._defaultDesc=defaultDesc || '&nbsp;';
+    var self=this;
+    this._sortfunc=function(v) { return self._sortvalue(v); };
+    this._codes=[];
+    this._descriptions=[];
+  },
+
+  _create: function(gridCell,windowRow) {
+    this._descriptions[windowRow]=Rico.createFormField(gridCell,'span',null,this.liveGrid.tableId+'_desc_'+this.index+'_'+windowRow);
+    this._codes[windowRow]=Rico.createFormField(gridCell,'input','hidden',this.liveGrid.tableId+'_code_'+this.index+'_'+windowRow);
+    this._clear(gridCell,windowRow);
+  },
+
+  _clear: function(gridCell,windowRow) {
+    this._codes[windowRow].value=this._defaultCode;
+    this._descriptions[windowRow].innerHTML='&nbsp;';
+  },
+
+  _sortvalue: function(v) {
+    return this._getdesc(v).replace(/&amp;/g, '&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&nbsp;/g,' ');
+  },
+
+  _getdesc: function(v) {
+    var desc=this._map[v];
+    return (typeof desc=='string') ? desc : this._defaultDesc;
+  },
+
+  _export: function(v) {
+    return this._getdesc(v);
+  },
+
+  _display: function(v,gridCell,windowRow) {
+    this._codes[windowRow].value=v;
+    this._descriptions[windowRow].innerHTML=this._getdesc(v);
+  }
+
+};
+
+
+
+Rico.TableColumn.MultiLine = function() {
+};
+
+Rico.TableColumn.MultiLine.prototype = {
+/**
+ * @class Fix issues with multiline content in IE
+ */
+  _display: function(v,gridCell,windowRow) {\r
+    var newdiv = document.createElement("div");\r
+    newdiv.innerHTML = this._format(v);\r
+    newdiv.style.height='100%';\r
+    if (gridCell.firstChild)\r
+      gridCell.replaceChild(newdiv, gridCell.firstChild);\r
+    else\r
+      gridCell.appendChild(newdiv);\r
+  }\r
+
+};
diff --git a/lib/rico3/minsrc/ricoLiveGridForms.js b/lib/rico3/minsrc/ricoLiveGridForms.js
new file mode 100644 (file)
index 0000000..5ce18dd
--- /dev/null
@@ -0,0 +1,1136 @@
+/*
+ *  (c) 2005-2011 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+if(typeof Rico=='undefined') throw("LiveGridForms requires the Rico JavaScript framework");
+
+
+Rico.TableEdit = function(liveGrid) {
+  this.initialize(liveGrid);
+}
+
+Rico.TableEdit.prototype = {
+/**
+ * @class Supports editing LiveGrid data.
+ * @constructs
+ */
+  initialize: function(liveGrid) {
+    Rico.log('Rico.TableEdit initialize: '+liveGrid.tableId);
+    this.grid=liveGrid;
+    this.options = {
+      maxDisplayLen    : 20,    // max displayed text field length
+      panelHeight      : 200,   // size of tabbed panels
+      panelWidth       : 500,
+      compact          : false,    // compact corners
+      RecordName       : Rico.getPhraseById("record"),
+      updateURL        : window.location.href, // default is that updates post back to the generating page
+      showSaveMsg      : 'errors'  // disposition of database update responses (full - show full response, errors - show full response for errors and short response otherwise)
+    };
+    Rico.extend(this.options, liveGrid.options);
+    var self=this;
+    this.menu=liveGrid.menu;
+    this.menu.options.dataMenuHandler=function(grid,r,c,onBlankRow) { return self.editMenu(grid,r,c,onBlankRow); };
+    this.menu.ignoreClicks();
+    this.editText=Rico.getPhraseById("editRecord",this.options.RecordName);
+    this.cloneText=Rico.getPhraseById("cloneRecord",this.options.RecordName);
+    this.delText=Rico.getPhraseById("deleteRecord",this.options.RecordName);
+    this.addText=Rico.getPhraseById("addRecord",this.options.RecordName);
+    this.buttonHover=new Rico.HoverSet();
+    this.dateRegExp=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
+    this.createKeyArray();
+    if (typeof(this.options.ConfirmDeleteCol) != 'number')
+      this.options.ConfirmDeleteCol=this.keys.length > 0 ? -2 : -1;
+    this.createEditDiv();
+    this.saveMsg=Rico.$(liveGrid.tableId+'_savemsg');
+    Rico.eventBind(document,"click", Rico.eventHandle(this,'clearSaveMsg'));
+    this.extraMenuItems=[];
+    this.responseHandler=function(xhr) { self.processResponse(xhr); };
+    Rico.log("Rico.TableEdit.initialize complete");
+  },
+
+  createKeyArray: function() {
+    this.keys=[];
+    for (var i=0; i<this.grid.columns.length; i++) {
+      if (this.grid.columns[i].format && this.grid.columns[i].format.isKey)
+        this.keys.push({colidx:i});
+    }
+  },
+
+  createEditDiv: function() {
+
+    // create popup form
+
+    this.requestCount=1;
+    this.formPopup=this.createWindow();
+    Rico.addClass(this.formPopup.content.parentNode,'ricoLG_editDiv');
+    if (this.options.canEdit || this.options.canAdd) {
+      this.startForm();
+      this.createForm(this.form);
+    } else {
+      var buttonClose=this.createButton(Rico.getPhraseById("close"));
+      Rico.eventBind(buttonClose,"click", Rico.eventHandle(this,'cancelEdit'), false);
+      this.createForm(this.formPopup.contentDiv);
+    }
+    this.editDivCreated=true;
+
+    // create responseDialog
+
+    this.responseDialog = this.grid.createDiv('editResponse',document.body);
+    this.responseDialog.style.display='none';
+
+    var buttonOK = document.createElement('button');
+    buttonOK.appendChild(document.createTextNode(Rico.getPhraseById("ok")));
+    Rico.eventBind(buttonOK,"click", Rico.eventHandle(this,'ackResponse'));
+    this.responseDialog.appendChild(buttonOK);
+
+    this.responseDiv = this.grid.createDiv('editResponseText',this.responseDialog);
+
+    if (this.panelGroup) {
+      Rico.log("createEditDiv complete, requestCount="+this.requestCount);
+      Rico.runLater(50,this,'initPanelGroup');
+    }
+  },
+  
+  createWindow: function() {
+    var self=this;
+    return new Rico.Window('', {closeFunc: function() { self.makeFormInvisible(); }, overflow: this.options.ColGroups ? 'hidden' : 'auto'});
+  },
+
+  initPanelGroup: function() {
+    this.requestCount--;
+    Rico.log("initPanelGroup: "+this.requestCount);
+    if (this.requestCount>0) return;
+    var wi=parseInt(this.options.panelWidth,10);
+    if (this.form) {
+      //this.form.style.width=(wi+10)+'px';
+      if (Rico.isWebKit) this.formPopup.container.style.display='block';  // this causes display to flash briefly
+      this.options.bgColor = Rico.Color.createColorFromBackground(this.form).toString();
+    }
+    this.formPopup.container.style.display='none';
+    this.formPanels=new Rico.TabbedPanel(this.panelGroup, this.options);
+  },
+
+  notEmpty: function(v) {
+    return typeof(v)!='undefined';
+  },
+
+  startForm: function() {
+    this.form = document.createElement('form');
+    /** @ignore */
+    this.form.onsubmit=function() {return false;};
+    this.form.autocomplete="off"; // seems to fix "Permission denied..." errors in FF
+    this.formPopup.contentDiv.appendChild(this.form);
+
+    var tab = document.createElement('div');
+    tab.className='ButtonBar';
+    var button=tab.appendChild(this.createButton(Rico.getPhraseById("saveRecord",this.options.RecordName)));
+    Rico.eventBind(button,"click", Rico.eventHandle(this,'TESubmit'), false);
+    button=tab.appendChild(this.createButton(Rico.getPhraseById("cancel")));
+    Rico.eventBind(button,"click", Rico.eventHandle(this,'cancelEdit'), false);
+    this.form.appendChild(tab);
+
+    // hidden fields
+    this.hiddenFields = document.createElement('div');
+    this.hiddenFields.style.display='none';
+    this.action = this.appendHiddenField(this.grid.actionId,'');
+    var i,fldSpec;
+    for (i=0; i<this.grid.columns.length; i++) {
+      fldSpec=this.grid.columns[i].format;
+      if (fldSpec && fldSpec.FormView && fldSpec.FormView=="hidden")
+        this.appendHiddenField(fldSpec.FieldName,fldSpec.ColData);
+    }
+    for (var k=0; k<this.keys.length; k++) {
+      this.keys[k].keyField = this.appendHiddenField('_k'+this.keys[k].colidx,'');
+    }
+    this.form.appendChild(this.hiddenFields);
+  },
+
+  createButton: function(buttonLabel) {
+    var button = document.createElement('a');
+    button.href='javascript:void(0)';
+    button.innerHTML=buttonLabel;
+    button.className='RicoButton';
+    if (Rico.theme.button) Rico.addClass(button,Rico.theme.button);
+    this.buttonHover.add(button);
+    return button;
+  },
+
+  createPanel: function(i) {
+    var hasFields=false;
+    for (var j=0; j<this.grid.columns.length; j++) {
+      var fldSpec=this.grid.columns[j].format;
+      if (!fldSpec) continue;
+      if (!fldSpec.EntryType) continue;
+      if (fldSpec.EntryType=='H') continue;
+      if (fldSpec.FormView && fldSpec.FormView=="hidden") continue;
+      var panelIdx=fldSpec.ColGroupIdx || 0;
+      if (panelIdx==i) {
+        hasFields=true;
+        break;
+      }
+    }
+    if (!hasFields) return null;
+    this.panelHdr[i] = document.createElement('li');
+    this.panelHdr[i].innerHTML=this.options.ColGroups[i];
+    this.panelHdrs.appendChild(this.panelHdr[i]);
+    this.panelContent[i] = document.createElement('div');
+    this.panelContents.appendChild(this.panelContent[i]);
+    this.panelActualIdx[i]=this.panelCnt++;
+    return this.createFormTable(this.panelContent[i],'tabContent');
+  },
+
+  createForm: function(parentDiv) {
+    var i,div,fldSpec,panelIdx,tables=[];
+    this.panelCnt=0;
+    this.panelHdr=[];
+    this.panelContent=[];
+    if (this.options.ColGroups) {
+      this.panelGroup = document.createElement('div');
+      this.panelGroup.className='tabPanelGroup';
+      this.panelHdrs = document.createElement('ul');
+      this.panelGroup.appendChild(this.panelHdrs);
+      this.panelContents = document.createElement('div');
+      this.panelContents.className='tabContentContainer';
+      this.panelGroup.appendChild(this.panelContents);
+      this.panelActualIdx=[];
+      parentDiv.appendChild(this.panelGroup);
+      if (this.grid.direction=='rtl') {
+        for (i=this.options.ColGroups.length-1; i>=0; i--) {
+          tables[i]=this.createPanel(i);
+        }
+      } else {
+        for (i=0; i<this.options.ColGroups.length; i++) {
+          tables[i]=this.createPanel(i);
+        }
+      }
+      parentDiv.appendChild(this.panelGroup);
+    } else {
+      div=document.createElement('div');
+      div.className='noTabContent';
+      tables[0]=this.createFormTable(div);
+      parentDiv.appendChild(div);
+    }
+    for (i=0; i<this.grid.columns.length; i++) {
+      fldSpec=this.grid.columns[i].format;
+      if (!fldSpec) continue;
+      panelIdx=fldSpec.ColGroupIdx || 0;
+      if (tables[panelIdx]) this.appendFormField(this.grid.columns[i],tables[panelIdx]);
+      if (typeof fldSpec.pattern=='string') {
+        switch (fldSpec.pattern) {
+          case 'email':
+            fldSpec.regexp=/^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.(([0-9]{1,3})|([a-zA-Z]{2,3})|(aero|coop|info|museum|name))$/;
+            break;
+          case 'float-unsigned':
+            fldSpec.regexp=/^\d+(\.\d+)?$/;
+            break;
+          case 'float-signed':
+            fldSpec.regexp=/^[-+]?\d+(\.\d+)?$/;
+            break;
+          case 'int-unsigned':
+            fldSpec.regexp=/^\d+$/;
+            break;
+          case 'int-signed':
+            fldSpec.regexp=/^[-+]?\d+$/;
+            break;
+          default:
+            fldSpec.regexp=new RegExp(fldSpec.pattern);
+            break;
+        }
+      }
+    }
+  },
+
+  createFormTable: function(div) {
+    var tab=document.createElement('table');
+    tab.border=0;
+    div.appendChild(tab);
+    return tab;
+  },
+
+  appendHiddenField: function(name,value) {
+    var field=Rico.createFormField(this.hiddenFields,'input','hidden',name,name);
+    field.value=value;
+    return field;
+  },
+
+  appendFormField: function(column, table) {
+    var fmt=column.format;
+    if (!fmt.EntryType) return;
+    if (fmt.EntryType=="H") return;
+    if (fmt.FormView) return;
+    Rico.log('appendFormField: '+column.displayName+' - '+fmt.EntryType);
+    var row = fmt.noFormBreak && table.rows.length > 0 ? table.rows[table.rows.length-1] : table.insertRow(-1);
+    var hdr = row.insertCell(-1);
+    column.formLabel=hdr;
+    if (hdr.noWrap) hdr.noWrap=true;
+    var entry = row.insertCell(-1);
+    if (entry.noWrap) entry.noWrap=true;
+    hdr.id='lbl_'+fmt.FieldName;
+    var field, name=fmt.FieldName;
+    switch (fmt.EntryType) {
+      case 'TA':
+      case 'tinyMCE':
+        field=Rico.createFormField(entry,'textarea',null,name);
+        field.cols=fmt.TxtAreaCols;
+        field.rows=fmt.TxtAreaRows;
+        field.innerHTML=fmt.ColData;
+        hdr.style.verticalAlign='top';
+        break;
+      case 'R':
+      case 'RL':
+        field=Rico.createFormField(entry,'div',null,name);
+        if (fmt.DescriptionField) field.RicoUpdate=fmt.DescriptionField;
+        if (fmt.MultiSelect) Rico.addClass(field, 'MultiSelect');
+        if (fmt.isNullable && !fmt.MultiSelect) this.addSelectNone(field);
+        this.selectValuesRequest(field,column);
+        break;
+      case 'N':
+        field=Rico.createFormField(entry,'select',null,name);
+        if (fmt.isNullable) this.addSelectNone(field);
+        Rico.eventBind(field,"change", Rico.eventHandle(this,'checkSelectNew'));
+        this.selectValuesRequest(field,column);
+        field=document.createElement('span');
+        field.className='ricoEditLabel';
+        field.id='labelnew__'+fmt.FieldName;
+        field.innerHTML='&nbsp;&nbsp;&nbsp;'+Rico.getPhraseById('formNewValue').replace(' ','&nbsp;');
+        entry.appendChild(field);
+        name='textnew__'+fmt.FieldName;
+        field=Rico.createFormField(entry,'input','text',name,name);
+        break;
+      case 'S':
+      case 'SL':
+        if (fmt.ReadOnly) {
+          field=Rico.createFormField(entry,'input','text',name,name);
+          this.initField(field,fmt);
+        } else {
+          field=Rico.createFormField(entry,'select',null,name);
+          if (fmt.MultiSelect) field.multiple=true;
+          if (fmt.SelectRows) field.size=parseInt(fmt.SelectRows,10);
+          if (fmt.isNullable && !fmt.MultiSelect) this.addSelectNone(field);
+          if (fmt.DescriptionField) {
+            field.RicoUpdate=fmt.DescriptionField;
+            Rico.eventBind(field,"change", Rico.eventHandle(this,'selectClick'), false);
+          }
+          this.selectValuesRequest(field,column);
+        }
+        break;
+      case 'D':
+        if (!fmt.isNullable) fmt.required=true;
+        if (!fmt.dateFmt) fmt.dateFmt=Rico.dateFmt;
+        if (!fmt.Help) fmt.Help=fmt.dateFmt;
+        if (typeof fmt.min=='string') fmt.min=Rico.setISO8601(fmt.min) || new Date(fmt.min);
+        if (typeof fmt.max=='string') fmt.max=Rico.setISO8601(fmt.max) || new Date(fmt.max);
+        fmt.Length=Math.max(fmt.dateFmt.length,10);
+        if (Rico.inputtypes.date) {
+          // use the WebForms calendar
+          field=Rico.createFormField(entry,'input','date',name,name);
+          field.required=fmt.required;
+          if (fmt.min) field.min=Rico.toISO8601String(fmt.min,3);
+          if (fmt.max) field.max=Rico.toISO8601String(fmt.max,3);
+          field.required=fmt.required;
+          fmt.SelectCtl=null;  // no need for Rico calendar control
+        } else {
+          field=Rico.createFormField(entry,'input','text',name,name);
+        }
+        this.initField(field,fmt);
+        break;
+      case 'I':
+        if (!fmt.isNullable) fmt.required=true;
+        if (!fmt.pattern) fmt.pattern='int-signed';
+        if (Rico.inputtypes.number) {
+          field=Rico.createFormField(entry,'input','number',name,name);
+          field.required=fmt.required;
+          field.min=fmt.min;
+          field.max=fmt.max;
+          field.step=1;
+        } else {
+          field=Rico.createFormField(entry,'input','text',name,name);
+        }
+        if (typeof fmt.min=='string') fmt.min=parseInt(fmt.min,10);
+        if (typeof fmt.max=='string') fmt.max=parseInt(fmt.max,10);
+        this.initField(field,fmt);
+        break;
+      case 'F':
+        if (!fmt.isNullable) fmt.required=true;
+        if (!fmt.pattern) fmt.pattern='float-signed';
+        field=Rico.createFormField(entry,'input','text',name,name);
+        this.initField(field,fmt);
+        if (typeof fmt.min=='string') fmt.min=parseFloat(fmt.min);
+        if (typeof fmt.max=='string') fmt.max=parseFloat(fmt.max);
+        break;
+      default:
+        field=Rico.createFormField(entry,'input','text',name,name);
+        if (!fmt.isNullable && fmt.EntryType!='T') fmt.required=true;
+        this.initField(field,fmt);
+        break;
+    }
+    if (field && fmt.SelectCtl) {
+      Rico.EditControls.applyTo(column,field,fmt.EntryType=='D');
+    }
+    var hdrSuffix='';
+    hdr.className='ricoEditLabel';
+    if (fmt.Help) {
+      hdr.title=fmt.Help;
+      hdrSuffix="&nbsp;<span class='rico-icon rico-info'></span>";
+    }
+    var hdrText=fmt.EntryType.length>1 && fmt.EntryType.charAt(1)=='L' ? column.next.displayName : column.displayName;
+    hdr.innerHTML=hdrText+hdrSuffix;
+  },
+
+  addSelectNone: function(field) {
+    this.addSelectOption(field,this.options.TableSelectNone,Rico.getPhraseById("selectNone"));
+  },
+
+  initField: function(field,fmt) {
+    if (fmt.Length) {
+      field.maxLength=fmt.Length;
+      field.size=Math.min(fmt.Length, this.options.maxDisplayLen);
+    }
+    field.value=fmt.ColData;
+  },
+  
+  selectClick: function(e) {
+    var SelObj=Rico.eventElement(e);
+    if (SelObj.readOnly) {
+      Rico.eventStop(e);
+      return false;
+    }
+    if (SelObj.RicoUpdate) {
+      var opt=SelObj.options[SelObj.selectedIndex];
+      Rico.$(SelObj.RicoUpdate).value=opt.innerHTML;
+    }
+  },
+  
+  radioClick: function(e) {
+    var ChkBoxObj=Rico.eventElement(e);
+    if (ChkBoxObj.readOnly) {
+      Rico.eventStop(e);
+      return false;
+    }
+    var container=Rico.getParentByTagName(ChkBoxObj,'div');
+    if (container.RicoUpdate) {
+      Rico.$(container.RicoUpdate).value=ChkBoxObj.nextSibling.innerHTML;
+    }
+  },
+
+  checkSelectNew: function(e) {
+    this.updateSelectNew(Rico.eventElement(e));
+  },
+
+  updateSelectNew: function(SelObj) {
+    var vis=(SelObj.value==this.options.TableSelectNew) ? "" : "hidden";
+    Rico.$("labelnew__" + SelObj.id).style.visibility=vis;
+    Rico.$("textnew__" + SelObj.id).style.visibility=vis;
+  },
+
+  selectValuesRequest: function(elem,column) {
+    var fldSpec=column.format;
+    if (fldSpec.SelectValues) {
+      var valueList=fldSpec.SelectValues.split(',');
+      for (var i=0; i<valueList.length; i++)
+        this.addSelectOption(elem,valueList[i],valueList[i],i);
+    } else {
+      this.requestCount++;
+      var options={}, self=this;
+      Rico.extend(options, this.grid.buffer.ajaxOptions);
+      options.parameters = this.grid.buffer.formQueryHashXML(0,-1);
+      options.parameters.edit = column.index;
+      options.onComplete = function(request) { self.selectValuesUpdate(elem,request); };
+      new Rico.ajaxRequest(this.grid.buffer.dataSource, options);
+      Rico.log("selectValuesRequest: "+fldSpec.FieldName);
+    }
+  },
+
+  selectValuesUpdate: function(elem,request) {
+    var response = request.responseXML.getElementsByTagName("ajax-response");
+    Rico.log("selectValuesUpdate: "+request.status);
+    if (response == null || response.length != 1) return;
+    response=response[0];
+    var error = response.getElementsByTagName('error');
+    if (error.length > 0) {
+      var errmsg=Rico.getContentAsString(error[0],this.grid.buffer.isEncoded);
+      Rico.log("Data provider returned an error:\n"+errmsg);
+      alert(Rico.getPhraseById("requestError",errmsg));
+      return;
+    }
+    response=response.getElementsByTagName('response')[0];
+    var rowsElement = response.getElementsByTagName('rows')[0];
+    var rows = this.grid.buffer.dom2jstable(rowsElement);
+    Rico.log("selectValuesUpdate: id="+elem.id+' rows='+rows.length);
+    for (var i=0; i<rows.length; i++) {
+      if (rows[i].length>0) {
+        var c0=rows[i][0];
+        var c1=(rows[i].length>1) ? rows[i][1] : c0;
+        this.addSelectOption(elem,c0,c1,i);
+      }
+    }
+    if (Rico.$('textnew__'+elem.id))
+      this.addSelectOption(elem,this.options.TableSelectNew,Rico.getPhraseById("selectNewVal"));
+    if (this.panelGroup)
+      Rico.runLater(50,this,'initPanelGroup');
+  },
+
+  addSelectOption: function(elem,value,text,idx) {
+    switch (elem.tagName.toLowerCase()) {
+      case 'div':
+        var opt=Rico.createFormField(elem,'input', Rico.hasClass(elem, 'MultiSelect') ? 'checkbox' : 'radio', elem.id+'_'+idx, elem.id);
+        opt.value=value;
+        var lbl=document.createElement('label');
+        lbl.innerHTML=text;
+        lbl.htmlFor=opt.id;
+        elem.appendChild(lbl);
+        Rico.eventBind(opt,"click", Rico.eventHandle(this,'radioClick'), false);
+        break;
+      case 'select':
+        Rico.addSelectOption(elem,value,text);
+        break;
+    }
+  },
+
+  clearSaveMsg: function() {
+    if (this.saveMsg) this.saveMsg.innerHTML="";
+  },
+
+  addMenuItem: function(menuText,menuAction,enabled) {
+    this.extraMenuItems.push({menuText:menuText,menuAction:menuAction,enabled:enabled});
+  },
+
+  editMenu: function(grid,r,c,onBlankRow) {
+    this.clearSaveMsg();
+    if (this.grid.buffer.sessionExpired==true || this.grid.buffer.startPos<0) return false;
+    this.rowIdx=r;
+    var elemTitle=Rico.$('pageTitle');
+    var pageTitle=elemTitle ? elemTitle.innerHTML : document.title;
+    this.menu.addMenuHeading(pageTitle);
+    var self=this;
+    if (onBlankRow==false) {
+      for (var i=0; i<this.extraMenuItems.length; i++) {
+        this.menu.addMenuItem(this.extraMenuItems[i].menuText,this.extraMenuItems[i].menuAction,this.extraMenuItems[i].enabled);
+      }
+      this.menu.addMenuItem(this.editText, function() { self.editRecord(); },this.canEdit(r));
+      this.menu.addMenuItem(this.delText, function() { self.deleteRecord(); },this.canDelete(r));
+      if (this.options.canClone) {
+        this.menu.addMenuItem(this.cloneText, function() { self.cloneRecord(); },this.canAdd(r) && this.canEdit(r));
+      }
+    }
+    this.menu.addMenuItem(this.addText, function() { self.addRecord(); },this.canAdd(r));
+    return true;
+  },
+  
+  canAdd: function(r) {
+    return (typeof this.options.canAdd=='function') ? this.options.canAdd(r) : this.options.canAdd;
+  },
+
+  canEdit: function(r) {
+    return (typeof this.options.canEdit=='function') ? this.options.canEdit(r) : this.options.canEdit;
+  },
+
+  canDelete: function(r) {
+    return (typeof this.options.canDelete=='function') ? this.options.canDelete(r) : this.options.canDelete;
+  },
+
+  cancelEdit: function(e) {
+    Rico.eventStop(e);
+    this.makeFormInvisible();
+    this.grid.highlightEnabled=true;
+    this.menu.cancelmenu();
+    return false;
+  },
+
+  setField: function(fldnum,fldvalue) {
+    var fldSpec=this.grid.columns[fldnum].format;
+    var e=Rico.$(fldSpec.FieldName);
+    var a,i,o,elems,opts,txt;
+    if (!e) return;
+    Rico.log('setField: '+fldSpec.FieldName+'='+fldvalue);
+    switch (e.tagName.toUpperCase()) {
+      case 'DIV':
+        elems=e.getElementsByTagName('INPUT');
+        o={}
+        if (fldSpec.MultiSelect && fldvalue) {
+          a=fldvalue.split(',');
+          for (var i=0; i<a.length; i++) o[a[i]]=1;
+        } else {
+          o[fldvalue]=1;
+        }
+        for (i=0; i<elems.length; i++)
+          elems[i].checked=o[elems[i].value]==1;
+        break;
+      case 'INPUT':
+        if (fldSpec.EntryType=='D') {
+          // remove time data if it exists
+          a=fldvalue.split(/\s|T/);
+          fldvalue=a[0];
+          if (this.isTextInput(e)) {
+            var d=fldvalue.toLowerCase() == 'today' ? new Date() : Rico.setISO8601(fldvalue);
+            if (d) fldvalue=Rico.formatDate(d,fldSpec.dateFmt);
+          }
+        }
+        e.value=fldvalue;
+        break;
+      case 'SELECT':
+        opts=e.options;
+        //alert('setField SELECT: id='+e.id+'\nvalue='+fldvalue+'\nopt cnt='+opts.length)
+        o={}
+        if (fldSpec.MultiSelect && fldvalue) {
+          a=fldvalue.split(',');
+          for (var i=0; i<a.length; i++) o[a[i]]=1;
+          for (i=0; i<opts.length; i++)
+            opts[i].selected=o[opts[i].value]==1;
+        } else {
+          for (i=0; i<opts.length; i++) {
+            if (opts[i].value==fldvalue) {
+              e.selectedIndex=i;
+              break;
+            }
+          }
+        }
+        if (fldSpec.EntryType=='N') {
+          txt=Rico.$('textnew__'+e.id);
+          if (!txt) alert('Warning: unable to find id "textnew__'+e.id+'"');
+          txt.value=fldvalue;
+          if (e.selectedIndex!=i) e.selectedIndex=opts.length-1;
+          this.updateSelectNew(e);
+        }
+        return;
+      case 'TEXTAREA':
+        e.value=fldvalue;
+        if (fldSpec.EntryType=='tinyMCE' && typeof(tinyMCE)!='undefined' && this.initialized) {
+          if (tinyMCE.updateContent) {
+            tinyMCE.updateContent(e.id);  // version 2.x
+          } else {
+            tinyMCE.execInstanceCommand(e.id, 'mceSetContent', false, fldvalue);  // version 3.x
+          }
+        }
+        return;
+    }
+  },
+
+  setReadOnly: function(action) {
+    for (var ro,i=0; i<this.grid.columns.length; i++) {
+      var fldSpec=this.grid.columns[i].format;
+      if (!fldSpec) continue;
+      var e=Rico.$(fldSpec.FieldName);
+      if (!e) continue;
+      switch (action) {
+        case 'ins': ro=!fldSpec.Writeable || fldSpec.ReadOnly || fldSpec.UpdateOnly; break;
+        case 'upd': ro=!fldSpec.Writeable || fldSpec.ReadOnly || fldSpec.InsertOnly; break;
+        default:    ro=false; break;
+      }
+      switch (e.tagName.toUpperCase()) {
+        case 'DIV':
+          var elems=e.getElementsByTagName('INPUT');
+          for (var j=0; j<elems.length; j++) {
+            elems[j].disabled=ro;
+          }
+          break;
+        case 'SELECT':
+          if (fldSpec.EntryType=='N') {
+            var txt=Rico.$('textnew__'+e.id);
+            txt.disabled=ro;
+          }
+          e.disabled=ro;
+          break;
+        case 'TEXTAREA':
+        case 'INPUT':
+          e.disabled=ro;
+          if (fldSpec.selectIcon) fldSpec.selectIcon.style.display=ro ? 'none' : '';
+          break;
+      }
+    }
+  },
+
+  hideResponse: function(msg) {
+    this.responseDiv.innerHTML=msg;
+    this.responseDialog.style.display='none';
+  },
+
+  showResponse: function() {
+    var offset=Rico.cumulativeOffset(this.grid.outerDiv);
+    offset.top+=Rico.docScrollTop();
+    this.responseDialog.style.top=offset.top+"px";
+    this.responseDialog.style.left=offset.left+"px";
+    this.responseDialog.style.display='';
+  },
+
+  processResponse: function(xhr) {
+    var responseText,success=true;
+    Rico.log('Processing response from form submittal: '+typeof(xhr));
+    this.responseDiv.innerHTML=xhr.responseText;
+    var respNodes=Rico.select('.ricoFormResponse',this.responseDiv);
+    if (respNodes) {
+      // generate a translated response
+      Rico.log('Found ricoFormResponse');
+      var phraseId=Rico.trim(respNodes[0].className).split(/\s+/)[1];
+      responseText=Rico.getPhraseById(phraseId,this.options.RecordName);
+    } else {
+      // present the response as sent from the server (untranslated)
+      Rico.log('Processing response text');
+      var ch=this.responseDiv.childNodes;
+      for (var i=ch.length-1; i>=0; i--) {
+        if (ch[i].nodeType==1 && ch[i].nodeName!='P' && ch[i].nodeName!='DIV' && ch[i].nodeName!='BR')
+          this.responseDiv.removeChild(ch[i]);
+      }
+      responseText=Rico.stripTags(this.responseDiv.innerHTML);
+      success=(responseText.toLowerCase().indexOf('error')==-1);
+    }
+    if (success && this.options.showSaveMsg!='full') {
+      this.hideResponse('');
+      this.grid.resetContents();
+      this.grid.buffer.foundRowCount = false;
+      this.grid.buffer.fetch(this.grid.lastRowPos || 0);
+      if (this.saveMsg) this.saveMsg.innerHTML='&nbsp;'+responseText+'&nbsp;';
+    }
+    this.processCallback(this.options.onSubmitResponse);
+    Rico.log('Processing response completed');
+  },
+
+  processCallback: function(callback) {
+    switch (typeof callback) {
+      case 'string': return eval(callback);
+      case 'function': return callback();
+    }
+  },
+
+  // called when ok pressed on error response message
+  ackResponse: function(e) {
+    this.hideResponse('');
+    this.grid.highlightEnabled=true;
+  },
+
+  cloneRecord: function() {
+    this.formPopup.setTitle(this.cloneText);
+    this.displayEditForm("ins");
+  },
+
+  editRecord: function() {
+    this.formPopup.setTitle(this.editText);
+    this.displayEditForm("upd");
+  },
+
+  displayEditForm: function(action) {
+    this.grid.highlightEnabled=false;
+    this.menu.cancelmenu();
+    this.hideResponse(Rico.getPhraseById('saving'));
+    this.grid.outerDiv.style.cursor = 'auto';
+    this.action.value=action;
+    for (var i=0; i<this.grid.columns.length; i++) {
+      var c=this.grid.columns[i];
+      if (c.format) {
+        var v=c.getValue(this.rowIdx);
+        this.setField(i,v);
+        if (c.format.selectDesc) {
+          if (c.format.EntryType.length>1 && c.format.EntryType.charAt(1)=='L')
+            v=this.grid.columns[i+1].getValue(this.rowIdx);
+          v=c._format(v);
+          if (v==='') v='&nbsp;';
+          c.format.selectDesc.innerHTML=v;
+        }
+        if (c.format.SelectCtl)
+          Rico.EditControls.displayClrImg(c, !c.format.InsertOnly);
+      }
+    }
+    this.setReadOnly(action);
+    for (var k=0; k<this.keys.length; k++) {
+      this.keys[k].keyField.value = this.grid.buffer.getWindowValue(this.rowIdx,this.keys[k].colidx);
+    }
+    this.makeFormVisible(this.rowIdx);
+  },
+  
+  addPrepare: function() {
+    this.hideResponse(Rico.getPhraseById('saving'));
+    this.form.reset();
+    this.setReadOnly("ins");
+    this.action.value="ins";
+    for (var i=0; i<this.grid.columns.length; i++) {
+      var c=this.grid.columns[i];
+      if (c.format) {
+        this.setField(i,c.format.ColData);
+        if (c.format.SelectCtl) {
+          if (c.format.EntryType != 'D') Rico.EditControls.resetValue(c);
+          Rico.EditControls.displayClrImg(c, !c.format.UpdateOnly);
+        }
+      }
+    }
+  },
+
+  addRecord: function() {
+    this.menu.cancelmenu();
+    this.formPopup.setTitle(this.addText);
+    this.addPrepare();
+    this.makeFormVisible(-1);
+    if (this.formPanels) this.formPanels.select(0);
+  },
+
+  drillDown: function(e,masterColNum,detailColNum) {
+    return this.grid.drillDown.apply(this.grid, arguments);
+  },
+
+  // set filter on a detail grid that is in a master-detail relationship
+  setDetailFilter: function(colNumber,filterValue) {
+    this.grid.setDetailFilter(colNumber,filterValue);
+  },
+
+  makeFormVisible: function(row) {
+    this.formPopup.container.style.display='block';
+
+    // set left position
+    var editWi=this.formPopup.container.offsetWidth;
+    var odOffset=Rico.cumulativeOffset(this.grid.outerDiv);
+    var winWi=Rico.windowWidth();
+    this.formPopup.container.style.left=editWi+odOffset.left > winWi ? (winWi-editWi)+'px' : (odOffset.left+1)+'px';
+
+    // set top position
+    var scrTop=Rico.docScrollTop();
+    var editHt=this.formPopup.container.offsetHeight;
+    var newTop=odOffset.top+this.grid.hdrHt+scrTop;
+    var bottom=Rico.windowHeight()+scrTop;
+    if (row >= 0) {
+      newTop+=(row+1)*this.grid.rowHeight;
+      if (newTop+editHt>bottom) newTop-=(editHt+this.grid.rowHeight);
+    } else {
+      if (newTop+editHt>bottom) newTop=bottom-editHt-2;
+    }
+
+    if (this.processCallback(this.options.formOpen) === false) return;
+    this.formPopup.openPopup(null,Math.max(newTop,scrTop));
+    this.formPopup.container.style.visibility='visible';
+    Rico.EditControls.setZ(Rico.getStyle(this.formPopup.container,'zIndex'));
+    if (this.initialized) return;
+
+    var i, spec;
+    for (i = 0; i < this.grid.columns.length; i++) {
+      spec=this.grid.columns[i].format;
+      if (!spec || !spec.EntryType || !spec.FieldName) continue;
+      switch (spec.EntryType) {
+        case 'tinyMCE':
+          if (typeof tinyMCE!='undefined') tinyMCE.execCommand('mceAddControl', true, spec.FieldName);
+          break;
+      }
+    }
+    this.initialized=true;
+  },
+
+  makeFormInvisible: function() {
+    for (var i=0; i<this.grid.columns.length; i++) {
+      if (this.grid.columns[i].format && this.grid.columns[i].format.SelectCtl)
+        Rico.EditControls.close(this.grid.columns[i].format.SelectCtl);
+    }
+    this.formPopup.container.style.visibility='hidden';
+    this.formPopup.closePopup();
+    this.processCallback(this.options.formClose);
+  },
+
+  getConfirmDesc: function(rowIdx) {
+    return Rico.stripTags(this.grid.cell(rowIdx,this.options.ConfirmDeleteCol).innerHTML).replace('&nbsp;',' '); //.unescapeHTML();
+  },
+
+  deleteRecord: function() {
+    this.menu.cancelmenu();
+    var desc;
+    switch(this.options.ConfirmDeleteCol){
+      case -1 :
+        desc=Rico.getPhraseById("thisRecord",this.options.RecordName);
+        break;
+      case -2 : // Use key/column header to identify the row
+        desc='';
+        for (var k=0; k<this.keys.length; k++) {
+          var i=this.keys[k].colidx;
+          var fmt=this.grid.columns[i].format;
+          if (fmt.EntryType.length>1 && fmt.EntryType.charAt(1)=='L') i++;
+          var value=Rico.trim(Rico.stripTags(this.grid.cell(this.rowIdx,i).innerHTML).replace(/&nbsp;/g,' '));
+          if (desc) desc+=', ';
+          desc+=this.grid.columns[i].displayName + ' \"' + value + '\"';
+        }
+        break;
+      default   :
+        desc='\"' + Rico.truncate(this.getConfirmDesc(this.rowIdx),50) + '\"';
+        break;
+    }
+    if (!this.options.ConfirmDelete.valueOf || confirm(Rico.getPhraseById("confirmDelete",desc))) {
+      this.hideResponse(Rico.getPhraseById('deleting'));
+      this.showResponse();
+      var parms={};
+      parms[this.grid.actionId]="del";
+      for (var k=0; k<this.keys.length; k++) {
+        var i=this.keys[k].colidx;
+        var value=this.grid.columns[i].getValue(this.rowIdx);
+        parms['_k'+i]=value;  // prototype does the encoding automatically
+        //parms['_k'+i]=encodeURIComponent(value);
+      }
+      new Rico.ajaxRequest(this.options.updateURL, {parameters:parms,method:'post',onComplete:this.responseHandler});
+    }
+    this.menu.cancelmenu();
+  },
+
+  validationMsg: function(elem,colnum,phraseId) {
+    var col=this.grid.columns[colnum];
+    if (this.formPanels) this.formPanels.select(this.panelActualIdx[col.format.ColGroupIdx]);
+    var label=Rico.stripTags(col.formLabel.innerHTML).replace(/&nbsp;/g,' ');
+    var msg=Rico.getPhraseById(phraseId," \"" + label + "\"");
+    Rico.log(' Validation error: '+msg);
+    if (col.format.Help) msg+="\n\n"+col.format.Help;
+    alert(msg);
+    setTimeout(function() { try { elem.focus(); elem.select(); } catch(e) {}; }, 10);
+    return false;
+  },
+  
+  isTextInput: function(elem) {
+    if (!elem) return false;
+    if (elem.tagName.toLowerCase()!='input') return false;
+    if (elem.type.toLowerCase()!='text') return false;
+    if (elem.readOnly) return false;
+    if (!Rico.visible(elem)) return false;
+    return true;
+  },
+  
+  parseDate: function(v, dateFmt) {
+    dateParts={};
+    if (!this.dateRegExp.exec(dateFmt)) return NaN;
+    dateParts[RegExp.$1]=0;
+    dateParts[RegExp.$3]=1;
+    dateParts[RegExp.$5]=2;
+    var aDate = v.split(/\D/);
+    var d=new Date();
+    var curyr=d.getFullYear();
+    if (aDate.length==2 && dateParts.yyyy==2) aDate.push(curyr);
+    if (aDate.length!=3) return NaN;
+    var dd=parseInt(aDate[dateParts.dd], 10);
+    if (dd==0 || dd>31) return NaN;
+    var mm=parseInt(aDate[dateParts.mm], 10) - 1;
+    if (mm > 11) return NaN;
+    var yy=parseInt(aDate[dateParts.yyyy], 10);
+    if (yy < 100) {
+      // apply a century to 2-digit years
+      yy+=curyr - (curyr % 100);
+    }
+    return new Date(yy,mm,dd,0,0,0);  // ensure time is midnight
+  },
+
+  TESubmit: function(e) {
+    var i,ro,lbl,spec,elem,n,dateValues=[];
+
+    Rico.eventStop(e);
+    Rico.log('Event: TESubmit called to validate input');
+
+    // check fields that are supposed to be non-blank
+
+    for (i = 0; i < this.grid.columns.length; i++) {
+      spec=this.grid.columns[i].format;
+      if (!spec || !spec.EntryType || !spec.FieldName) continue;
+      elem=Rico.$(spec.FieldName);
+      if (!this.isTextInput(elem)) continue;
+      switch (this.action.value) {
+        case 'ins': ro=!spec.Writeable || spec.ReadOnly || spec.UpdateOnly; break;
+        case 'upd': ro=!spec.Writeable || spec.ReadOnly || spec.InsertOnly; break;
+        default:    ro=false; break;
+      }
+      if (ro) continue;  // readonly, so don't validate
+      Rico.log(' Validating field #'+i+' EntryType='+spec.EntryType+' ('+spec.FieldName+')');
+
+      // check for blanks
+      if (elem.value.length == 0) {
+        if (spec.required)
+          return this.validationMsg(elem,i,"formPleaseEnter");
+        else
+          continue;
+      }
+
+      // check pattern
+      if (elem.value.length > 0 && spec.regexp && !spec.regexp.test(elem.value))
+        return this.validationMsg(elem,i,"formInvalidFmt");
+
+      // check min/max and date values
+      switch (spec.EntryType.charAt(0)) {
+        case 'I': n=parseInt(elem.value,10); break;
+        case 'F': n=parseFloat(elem.value); break;
+        case 'D': 
+          n=this.parseDate(elem.value,spec.dateFmt);
+          if (isNaN(n)) return this.validationMsg(elem,i,"formInvalidFmt");
+          dateValues.push({e:elem,v:n});
+          break;
+        default:  n=NaN; break;
+      }
+      if (typeof spec.min!='undefined' && !isNaN(n) && n < spec.min)
+        return this.validationMsg(elem,i,"formOutOfRange");
+      if (typeof spec.max!='undefined' && !isNaN(n) && n > spec.max)
+        return this.validationMsg(elem,i,"formOutOfRange");
+    }
+    if (this.processCallback(this.options.formSubmit) === false) return false;
+
+    // update drop-down for any columns with entry type of N
+
+    for (i = 0; i < this.grid.columns.length; i++) {
+      spec=this.grid.columns[i].format;
+      if (!spec || !spec.EntryType || !spec.FieldName) continue;
+      if (spec.EntryType.charAt(0) != 'N') continue;
+      var SelObj=Rico.$(spec.FieldName);
+      if (!SelObj || SelObj.value!=this.options.TableSelectNew) continue;
+      var newtext=Rico.$("textnew__" + SelObj.id).value;
+      this.addSelectOption(SelObj,newtext,newtext);
+    }
+    
+    // set date values to ISO format
+    for (i = 0; i < dateValues.length; i++) {
+      dateValues[i].e.value = Rico.formatDate(dateValues[i].v,'yyyy-mm-dd');
+    }
+
+    if (typeof tinyMCE!='undefined') tinyMCE.triggerSave();
+    this.makeFormInvisible();
+    this.sendForm();
+    this.menu.cancelmenu();
+    return false;
+  },
+  
+  sendForm: function() {
+    this.setReadOnly("reset");  // reset disabled flag so that all fields are sent to server
+    this.showResponse();
+    Rico.log("sendForm: "+this.grid.tableId);
+    Rico.ajaxSubmit(this.form, this.options.updateURL, {method:'post',onComplete:this.responseHandler});
+  }
+};
+
+
+/**
+ * @namespace Registers custom popup widgets to fill in a text box (e.g. ricoCalendar and ricoTree)
+ * <pre>
+ * Custom widget must implement:
+ *   open() method (make control visible)
+ *   close() method (hide control)
+ *   container property (div element that contains the control)
+ *   id property (uniquely identifies the widget class)
+ *
+ * widget calls returnValue method to return a value to the caller
+ *
+ * this object handles clicks on the control's icon and positions the control appropriately.
+ * </pre>
+ */
+Rico.EditControls = {
+  widgetList : {},
+  elemList   : {},
+  zIndex     : 0,
+
+  register: function(widget, imgsrc) {
+    this.widgetList[widget.id] = {imgsrc:imgsrc, widget:widget, currentEl:''};
+    var self=this;
+    widget.returnValue=function(newVal,newDesc) { self.setValue(widget,newVal,newDesc); };
+    Rico.log("Rico.EditControls.register:"+widget.id);
+  },
+  
+  setZ: function(z) {
+    this.zIndex=Math.max(this.zIndex,z+10);
+  },
+
+  applyTo: function(column,inputCtl,showInput) {
+    var wInfo=this.widgetList[column.format.SelectCtl];
+    if (!wInfo) return;
+    Rico.log('Rico.EditControls.applyTo: '+column.displayName+' : '+column.format.SelectCtl);
+    var newimg, descSpan = document.createElement('span');
+    if (wInfo.imgsrc.indexOf('.')==-1 && wInfo.imgsrc.indexOf('/')==-1) {
+      // treat imgsrc as a class name
+      newimg = document.createElement('span');
+      newimg.className=wInfo.imgsrc;
+    } else {
+      // treat imgsrc as an image uri
+      newimg = document.createElement('img');
+      newimg.src=wInfo.imgsrc;
+    }
+    newimg.style.verticalAlign='top';
+    newimg.style.marginLeft='4px';
+    newimg.style.cursor='pointer';
+    newimg.id=this.imgId(column.format.FieldName);
+    Rico.eventBind(newimg,"click", Rico.eventHandle(this,'processClick'));
+    inputCtl.parentNode.appendChild(descSpan);
+    inputCtl.parentNode.appendChild(newimg);
+    if (showInput) {
+      descSpan.style.display='none'; 
+    } else {
+      inputCtl.style.display='none';    // comment out this line for debugging
+    }
+    var clr;
+    if (column.format.isNullable) {
+      clr=Rico.clearButton(Rico.eventHandle(this,'processClear'));
+      clr.id=newimg.id+'_clear';
+      inputCtl.parentNode.appendChild(clr);
+    }
+    this.elemList[newimg.id] = {descSpan:descSpan, inputCtl:inputCtl, widget:wInfo.widget, listObj:wInfo, column:column, clrimg:clr};
+    column.format.selectIcon=newimg;
+    column.format.selectDesc=descSpan;
+  },
+
+  displayClrImg: function(column,bShow) {
+    var el=this.elemList[this.imgId(column.format.FieldName)];
+    //alert(column.format.FieldName+': '+bShow+' '+el.clrimg.id);
+    if (el && el.clrimg) el.clrimg.style.display=bShow ? 'inline-block' : 'none';
+  },
+
+  processClear: function(e) {
+    var elem=Rico.eventElement(e);
+    var el=this.elemList[elem.id.slice(0,-6)];
+    if (!el) return;
+    el.inputCtl.value='';
+    el.descSpan.innerHTML=el.column._format('');
+  },
+
+  processClick: function(e) {
+    var elem=Rico.eventElement(e);
+    var el=this.elemList[elem.id];
+    if (!el) return;
+    if (el.listObj.currentEl==elem.id && el.widget.container.style.display!='none') {
+      el.widget.close();
+      el.listObj.currentEl='';
+    } else {
+      el.listObj.currentEl=elem.id;
+      Rico.log('Rico.EditControls.processClick: '+el.widget.id+' : '+el.inputCtl.value);
+      el.widget.container.style.zIndex=this.zIndex;
+      el.widget.open(el.inputCtl.value,el.column);     // this may change the size of the widget
+      Rico.positionCtlOverIcon(el.widget.container,elem);
+    }
+  },
+
+  imgId: function(fieldname) {
+    return 'icon_'+fieldname;
+  },
+
+  resetValue: function(column) {
+    var el=this.elemList[this.imgId(column.format.FieldName)];
+    if (!el) return;
+    el.inputCtl.value=column.format.ColData;
+    var v=column._format(column.format.ColData);
+    if (v==='') v='&nbsp;';
+    el.descSpan.innerHTML=v;
+  },
+
+  setValue: function(widget,newVal,newDesc) {
+    var wInfo=this.widgetList[widget.id];
+    if (!wInfo) return null;
+    var id=wInfo.currentEl;
+    if (!id) return null;
+    var el=this.elemList[id];
+    if (!el) return null;
+    el.inputCtl.value=newVal;
+    if (!newDesc) newDesc=el.column._format(newVal);
+    el.descSpan.innerHTML=newDesc;
+    if (el.column.format.DescriptionField)
+      Rico.$(el.column.format.DescriptionField).value = newDesc;
+    //alert(widget.id+':'+id+':'+el.inputCtl.id+':'+el.inputCtl.value+':'+newDesc);
+  },
+
+  close: function(id) {
+    var wInfo=this.widgetList[id];
+    if (!wInfo) return;
+    if (wInfo.widget.container.style.display!='none')
+      wInfo.widget.close();
+  }
+};
diff --git a/lib/rico3/minsrc/ricoLiveGridMenu.js b/lib/rico3/minsrc/ricoLiveGridMenu.js
new file mode 100644 (file)
index 0000000..406e7ce
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ *  (c) 2005-2011 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2011 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+/**
+ * Standard menu for LiveGrid
+ */
+Rico.GridMenu = function(options) {
+  this.initialize(options);
+};
+
+Rico.GridMenu.prototype = {
+
+initialize: function(options) {
+  this.options = {
+    width           : '18em',
+    dataMenuHandler : null          // put custom items on the menu
+  };
+  Rico.extend(this.options, options || {});
+  Rico.extend(this, new Rico.Menu(this.options));
+  this.sortmenu = new Rico.Menu({ width: '15em' });
+  this.filtermenu = new Rico.Menu({ width: '22em' });
+  this.exportmenu = new Rico.Menu({ width: '24em' });
+  this.hideshowmenu = new Rico.Menu({ width: '22em' });
+  this.createDiv();
+  this.sortmenu.createDiv();
+  this.filtermenu.createDiv();
+  this.exportmenu.createDiv();
+  this.hideshowmenu.createDiv();
+},
+
+// Build context menu for grid
+buildGridMenu: function(r,c) {
+  this.clearMenu();
+  var livegrid=this.liveGrid;
+  var buffer=livegrid.buffer;
+  var totrows=buffer.totalRows;
+  var maxprint=livegrid.options.maxPrint;
+  var onBlankRow=(r >= totrows);
+  var column=livegrid.columns[c];
+  if (this.options.dataMenuHandler) {
+     var showDefaultMenu=this.options.dataMenuHandler(livegrid,r,c,onBlankRow);
+     if (!showDefaultMenu) return (this.itemCount > 0);
+  }
+
+  // menu items for sorting
+  if (column.sortable && totrows>0) {
+    this.sortmenu.clearMenu();
+    this.addSubMenuItem(Rico.getPhraseById("gridmenuSortBy",column.displayName), this.sortmenu, false);
+    this.sortmenu.addMenuItemId("gridmenuSortAsc", function() { column.sortAsc(); }, true);
+    this.sortmenu.addMenuItemId("gridmenuSortDesc", function() { column.sortDesc(); }, true);
+  }
+
+  // menu items for filtering
+  this.filtermenu.clearMenu();
+  if (column.canFilter()) {
+    this.addSubMenuItem(Rico.getPhraseById("gridmenuFilterBy",column.displayName), this.filtermenu, false);
+    if (!column.format.filterUI && (!onBlankRow || column.filterType == Rico.ColumnConst.USERFILTER)) {
+      column.userFilter=column.getValue(r);
+      if (column.filterType == Rico.ColumnConst.USERFILTER) {
+        this.filtermenu.addMenuItemId("gridmenuRemoveFilter", function() { column.setUnfiltered(false); }, true);
+        if (column.filterOp=='LIKE')
+          this.filtermenu.addMenuItemId("gridmenuChgKeyword", function() { livegrid.openKeyword(c); }, true);
+        if (column.filterOp=='NE' && !onBlankRow)
+          this.filtermenu.addMenuItemId("gridmenuExcludeAlso", function() { column.addFilterNE(); }, true);
+      } else if (!onBlankRow) {
+        this.filtermenu.addMenuItemId("gridmenuInclude", function() { column.setFilterEQ(); }, true);
+        this.filtermenu.addMenuItemId("gridmenuGreaterThan", function() { column.setFilterGE(); }, column.userFilter!='');
+        this.filtermenu.addMenuItemId("gridmenuLessThan", function() { column.setFilterLE(); }, column.userFilter!='');
+        if (column.isText)
+          this.filtermenu.addMenuItemId("gridmenuContains", function() { livegrid.openKeyword(c); }, true);
+        this.filtermenu.addMenuItemId("gridmenuExclude", function() { column.setFilterNE(); }, true);
+      }
+    }
+    if (livegrid.filterCount() > 0) {
+      this.filtermenu.addMenuItemId("gridmenuRemoveAll", function() { livegrid.clearFilters(); }, true);
+    }
+    if (buffer.options.canRefresh) this.filtermenu.addMenuItemId("gridmenuRefresh", function() { livegrid.filterHandler(); }, true);
+  }
+
+  // menu items for Print/Export
+  this.exportmenu.clearMenu();
+  if (maxprint > 0) {
+    this.addSubMenuItem(Rico.getPhraseById('gridmenuExport'),this.exportmenu,false);
+    if (buffer.printVisibleSQL && typeof(buffer.dataSource)=='string') {
+      // SQL buffer
+      this.exportmenu.addMenuItemId("gridmenuExportVis2Web", function() { buffer.printVisibleSQL('html'); });
+      this.exportmenu.addMenuItemId("gridmenuExportAll2Web", function() { buffer.printAllSQL('html'); }, buffer.totalRows <= maxprint);
+      this.exportmenu.addMenuBreak();
+      this.exportmenu.addMenuItemId("gridmenuExportVis2SS", function() { buffer.printVisibleSQL('xl'); });
+      this.exportmenu.addMenuItemId("gridmenuExportAll2SS", function() { buffer.printAllSQL('xl'); }, buffer.totalRows <= maxprint);
+    } else {
+      // any other buffer
+      this.exportmenu.addMenuItemId("gridmenuExportVis2Web", function() { buffer.printVisible(); });
+      this.exportmenu.addMenuItemId("gridmenuExportAll2Web", function() { buffer.printAll(); }, buffer.totalRows <= maxprint);
+    }
+  }
+
+  // menu items for hide/unhide
+  var hiddenCols=livegrid.listInvisible();
+  for (var showableCnt=0,x=0; x<hiddenCols.length; x++) {
+    if (hiddenCols[x].canHideShow()) showableCnt++;
+  }
+  if (showableCnt > 0 || column.canHideShow()) {
+    this.hideshowmenu.clearMenu();
+    this.addSubMenuItem(Rico.getPhraseById('gridmenuHideShow'),this.hideshowmenu,false);
+    this.hideshowmenu.addMenuItemId('gridmenuChooseCols', function() { livegrid.chooseColumns(); },true,false);
+    var visibleCnt=livegrid.columns.length-hiddenCols.length;
+    var enabled=(visibleCnt>1 && column.visible && column.canHideShow());
+    this.hideshowmenu.addMenuItem(Rico.getPhraseById('gridmenuHide',column.displayName), function() { column.hideColumn(); }, enabled);
+    if (hiddenCols.length > 1)
+      this.hideshowmenu.addMenuItemId('gridmenuShowAll', function() { livegrid.showAll(); });
+  }
+  return true;
+}
+
+}
diff --git a/lib/rico3/minsrc/ricoLocale_en.js b/lib/rico3/minsrc/ricoLocale_en.js
new file mode 100644 (file)
index 0000000..8a1ce27
--- /dev/null
@@ -0,0 +1,124 @@
+/*****************************************************************
+ ricoLocale_en.js - a component of Rico 3
+ English localization strings
+ This file is the basis for all translations
+
+ If you would like to include translations for another language, 
+ please post them on http://sourceforge.net/projects/openrico/
+******************************************************************/
+Rico.langCode='en';
+
+// used in ricoLiveGrid.js
+
+Rico.addPhraseId('bookmarkExact',"Listing records $1 - $2 of $3");
+Rico.addPhraseId('bookmarkAbout',"Listing records $1 - $2 of more than $3");
+Rico.addPhraseId('bookmarkNoRec',"No records");
+Rico.addPhraseId('bookmarkNoMatch',"No matching records");
+Rico.addPhraseId('bookmarkLoading',"Loading...");
+Rico.addPhraseId('sorting',"Sorting...");
+Rico.addPhraseId('exportStatus',"Exporting row $1");
+Rico.addPhraseId('filterAll',"(all)");
+Rico.addPhraseId('filterBlank',"(blank)");
+Rico.addPhraseId('filterEmpty',"(empty)");
+Rico.addPhraseId('filterNotEmpty',"(not empty)");
+Rico.addPhraseId('filterLike',"contains: $1");
+Rico.addPhraseId('filterNot',"not: $1");
+Rico.addPhraseId('requestError',"The request for data returned an error:\n$1");
+Rico.addPhraseId('keywordPrompt',"Enter keyword to search for (use * as a wildcard):");
+Rico.addPhraseId('keywordTitle',"Keyword Search");
+Rico.addPhraseId('apply',"Apply");
+
+// used in ricoLiveGridMenu.js
+
+Rico.addPhraseId('gridmenuSortBy',"Sort by: $1");
+Rico.addPhraseId('gridmenuSortAsc',"Ascending");
+Rico.addPhraseId('gridmenuSortDesc',"Descending");
+Rico.addPhraseId('gridmenuFilterBy',"Filter by: $1");
+Rico.addPhraseId('gridmenuRefresh',"Refresh");
+Rico.addPhraseId('gridmenuChgKeyword',"Change keyword...");
+Rico.addPhraseId('gridmenuExcludeAlso',"Exclude this value also");
+Rico.addPhraseId('gridmenuInclude',"Include only this value");
+Rico.addPhraseId('gridmenuGreaterThan',"Greater than or equal to this value");
+Rico.addPhraseId('gridmenuLessThan',"Less than or equal to this value");
+Rico.addPhraseId('gridmenuContains',"Contains keyword...");
+Rico.addPhraseId('gridmenuExclude',"Exclude this value");
+Rico.addPhraseId('gridmenuRemoveFilter',"Remove filter");
+Rico.addPhraseId('gridmenuRemoveAll',"Remove all filters");
+
+Rico.addPhraseId('gridmenuExport',"Print/Export");
+Rico.addPhraseId('gridmenuExportVis2Web',"Visible rows to web page");
+Rico.addPhraseId('gridmenuExportAll2Web',"All rows to web page");
+Rico.addPhraseId('gridmenuExportVis2SS',"Visible rows to spreadsheet");
+Rico.addPhraseId('gridmenuExportAll2SS',"All rows to spreadsheet");
+
+Rico.addPhraseId('gridmenuHideShow',"Hide/Show");
+Rico.addPhraseId('gridmenuChooseCols',"Choose columns...");
+Rico.addPhraseId('gridmenuHide',"Hide: $1");
+Rico.addPhraseId('gridmenuShow',"Show: $1");
+Rico.addPhraseId('gridmenuShowAll',"Show All");
+
+// used in ricoLiveGridAjax.js
+
+Rico.addPhraseId('sessionExpireMinutes',"minutes before your session expires");
+Rico.addPhraseId('sessionExpired',"EXPIRED");
+Rico.addPhraseId('requestTimedOut',"Request for data timed out!");
+Rico.addPhraseId('waitForData',"Waiting for data...");
+Rico.addPhraseId('httpError',"Received HTTP error: $1");
+Rico.addPhraseId('invalidResponse',"Server returned an invalid response");
+
+// used in ricoLiveGridCommon.js
+
+Rico.addPhraseId('gridChooseCols',"Choose columns");
+Rico.addPhraseId('exportComplete',"Exporting complete");
+Rico.addPhraseId('exportInProgress',"Export in progress...");
+Rico.addPhraseId('disableBlocker',"You need to disable your browser's pop-up blocker before exporting.");
+Rico.addPhraseId('showFilterRow',"Show filter row");  // img alt text
+Rico.addPhraseId('hideFilterRow',"Hide filter row");  // img alt text
+
+// used in ricoLiveGridForms.js
+
+Rico.addPhraseId('ok',"OK");
+Rico.addPhraseId('selectNone',"(none)");
+Rico.addPhraseId('selectNewVal',"(new value)");
+Rico.addPhraseId('record',"record");
+Rico.addPhraseId('thisRecord',"this $1");
+Rico.addPhraseId('confirmDelete',"Are you sure you want to delete $1?");
+Rico.addPhraseId('deleting',"Deleting...");
+Rico.addPhraseId('formPleaseEnter',"Please enter a value for $1");
+Rico.addPhraseId('formInvalidFmt',"Invalid format for $1");
+Rico.addPhraseId('formOutOfRange',"Value is out of range for $1");
+Rico.addPhraseId('formNewValue',"new value:");
+Rico.addPhraseId('saving',"Saving...");
+Rico.addPhraseId('clear',"clear");
+Rico.addPhraseId('close',"Close");
+Rico.addPhraseId('saveRecord',"Save $1");
+Rico.addPhraseId('cancel',"Cancel");
+Rico.addPhraseId('editRecord',"Edit $1");
+Rico.addPhraseId('deleteRecord',"Delete this $1");
+Rico.addPhraseId('cloneRecord',"Clone $1");
+Rico.addPhraseId('addRecord',"Add new $1");
+Rico.addPhraseId('addedSuccessfully',"$1 added successfully");
+Rico.addPhraseId('deletedSuccessfully',"$1 deleted successfully");
+Rico.addPhraseId('updatedSuccessfully',"$1 updated successfully");
+
+// used in ricoTree.js
+
+Rico.addPhraseId('treeSave',"Save Selection");
+Rico.addPhraseId('treeClear',"Clear All");
+
+// used in ricoCalendar.js
+
+Rico.addPhraseId('calToday',"Today is $1 $2 $3");  // $1=day, $2=monthabbr, $3=year, $4=month number
+Rico.addPhraseId('calWeekHdg',"Wk");
+Rico.addPhraseId('calYearRange',"Year ($1-$2)");
+Rico.addPhraseId('calInvalidYear',"Invalid year");
+\r
+// Date & number formats\r
+\r
+Rico.thouSep=","\r
+Rico.decPoint="."\r
+Rico.dateFmt="mm/dd/yyyy"\r
+\r
+Rico.monthNames=['January','February','March','April','May','June','July','August','September','October','November','December']\r
+Rico.dayNames=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']\r
diff --git a/lib/rico3/minsrc/ricoUI.js b/lib/rico3/minsrc/ricoUI.js
new file mode 100644 (file)
index 0000000..48f0662
--- /dev/null
@@ -0,0 +1,1382 @@
+/*
+ *  (c) 2005-2009 Richard Cowin (http://openrico.org)
+ *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ *  file except in compliance with the License. You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software distributed under the
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ *  either express or implied. See the License for the specific language governing permissions
+ *  and limitations under the License.
+ */
+
+Rico.applyShadow = function(elem,shadowFlag) {
+  if (typeof shadowFlag=='undefined') shadowFlag=true;
+  if (shadowFlag) Rico.addClass(elem,'ricoShadow');
+  return elem;
+};
+
+// ensure popups/windows get closed in the right order when the user hits escape key
+Rico._OpenPopupList = [];
+Rico._RemoveOpenPopup = function(popup) {
+  if (popup.openIndex >= 0 && popup.openIndex < Rico._OpenPopupList.length) Rico._OpenPopupList.splice(popup.openIndex,1);
+  popup.openIndex = -1;
+};
+Rico._AddOpenPopup = function(popup) {
+  popup.openIndex = Rico._OpenPopupList.push(popup) - 1;
+};
+Rico._checkEscKey = function(e) {
+  if (Rico.eventKey(e) != 27) return true;
+  while (Rico._OpenPopupList.length > 0) {
+    var popup = Rico._OpenPopupList.pop();
+    if (popup && popup.visible()) {
+      popup.openIndex = -1;
+      Rico.eventStop(e);
+      popup.closeFunc();
+      return false;
+    }
+  }
+  return true;
+};
+Rico.eventBind(document,"keyup", Rico.eventHandle(Rico,'_checkEscKey'));
+
+
+Rico.Popup = function(containerDiv,options) {
+  this.initialize(containerDiv,options);
+};
+
+Rico.Popup.prototype = {
+/**
+ * @class Class to manage pop-up div windows.
+ * @constructs
+ * @param options object may contain any of the following:<dl>
+ *   <dt>hideOnClick </dt><dd> hide popup when mouse button is clicked? default=true</dd>
+ *   <dt>ignoreClicks</dt><dd> if true, mouse clicks within the popup are not allowed to bubble up to parent elements</dd>
+ *   <dt>position    </dt><dd> defaults to absolute, use "auto" to auto-detect</dd>
+ *   <dt>shadow      </dt><dd> display shadow with popup? default=true</dd>
+ *   <dt>zIndex      </dt><dd> which layer? default=1</dd>
+ *   <dt>canDrag     </dt><dd> boolean value (or function that returns a boolean) indicating if it is ok to drag/reposition popup, default=false</dd>
+ *   <dt>onClose     </dt><dd> function to call when the popup is closed</dd>
+ *</dl>
+ * @param containerDiv if supplied, then setDiv() is called at the end of initialization
+ */
+  initialize: function(containerDiv,options) {
+    this.options = {
+      hideOnClick   : false,
+      ignoreClicks  : false,
+      position      : 'absolute',
+      shadow        : true,
+      zIndex        : 2,
+      canDrag       : false,
+      dragElement   : false,
+      closeFunc     : false
+    };
+    this.openIndex=-1;
+    if (containerDiv) this.setDiv(containerDiv,options);
+  },
+
+  createContainer: function(options) {
+    this.setDiv(document.createElement('div'), options);
+    if (options && options.parent) {
+      options.parent.appendChild(this.container);
+    } else {
+      document.getElementsByTagName("body")[0].appendChild(this.container);
+    }
+  },
+
+/**
+ * Apply popup behavior to a div that already exists in the DOM
+ * @param containerDiv div element (or element id) in the DOM. If null, then the div is created automatically.
+ */
+  setDiv: function(containerDiv,options) {
+    Rico.extend(this.options, options || {});
+    this.container=Rico.$(containerDiv);
+    if (this.options.position == 'auto') {
+      this.position=Rico.getStyle(this.container,'position').toLowerCase();
+    } else {
+      this.position=this.container.style.position=this.options.position;
+    }
+    this.content=document.createElement('div');
+    while (this.container.firstChild) {
+      this.content.appendChild(this.container.firstChild);
+    }
+    this.container.appendChild(this.content);
+    this.content.className='RicoPopupContent';
+    if (this.position != 'absolute') return;
+
+    if (this.options.closeFunc) {
+      this.closeFunc=this.options.closeFunc;
+    } else {
+      var self=this;
+      this.closeFunc=function() { self.closePopup(); };
+    }
+    this.container.style.top='0px';
+    this.container.style.left='0px';
+    this.container.style.display='none';
+    if (this.options.zIndex >= 0) this.container.style.zIndex=this.options.zIndex;
+
+    if (Rico.isIE && Rico.ieVersion < 7 && this.options.shim!==false) {
+      this.content.style.position='relative';
+      this.content.style.zIndex=2;
+      // create iframe shim
+      this.ifr = document.createElement('iframe');
+      this.ifr.className='RicoShim';
+      this.ifr.frameBorder=0;
+      this.ifr.src="javascript:'';";
+      this.container.appendChild(this.ifr);
+    }
+    Rico.applyShadow(this.container,this.options.shadow);
+
+    if (this.options.hideOnClick)
+      Rico.eventBind(document,"click", Rico.eventHandle(this,'_docClick'));
+    this.dragEnabled=false;
+    this.mousedownHandler = Rico.eventHandle(this,'_startDrag');
+    this.dragHandler = Rico.eventHandle(this,'_drag');
+    this.dropHandler = Rico.eventHandle(this,'_endDrag');
+    if (this.options.canDrag) this.enableDragging();
+    if (this.options.ignoreClicks || this.options.canDrag) this.ignoreClicks();
+  },
+
+  clearContent: function() {
+    this.content.innerHTML="";
+  },
+
+  setContent: function(content) {
+    this.content.innerHTML=content;
+  },
+
+  enableDragging: function() {
+    if (!this.dragEnabled && this.options.dragElement) {
+      Rico.eventBind(this.options.dragElement, "mousedown", this.mousedownHandler);
+      this.dragEnabled=true;
+    }
+    return this.dragEnabled;
+  },
+
+  disableDragging: function() {
+    if (!this.dragEnabled) return;
+    Rico.eventUnbind(this.options.dragElement, "mousedown", this.mousedownHandler);
+    this.dragEnabled=false;
+  },
+
+  setZ: function(zIndex) {
+    this.container.style.zIndex=zIndex;
+  },
+
+/** @private */
+  ignoreClicks: function() {
+    Rico.eventBind(this.container,"click", Rico.eventHandle(this,'_ignoreClick'));
+  },
+
+  _ignoreClick: function(e) {
+    if (e.stopPropagation)
+      e.stopPropagation();
+    else
+      e.cancelBubble = true;
+    return true;
+  },
+
+  _docClick: function(e) {
+    this.closeFunc();
+    return true;
+  },
+
+/**
+ * Move popup to specified position
+ */
+  move: function(left,top) {
+    if (typeof left=='number') this.container.style.left=left+'px';
+    if (typeof top=='number') this.container.style.top=top+'px';
+  },
+
+  _startDrag : function(event){
+    var elem=Rico.eventElement(event);
+    this.container.style.cursor='move';
+    this.lastMouse = Rico.eventClient(event);
+    Rico.eventBind(document, "mousemove", this.dragHandler);
+    Rico.eventBind(document, "mouseup", this.dropHandler);
+    Rico.eventStop(event);
+  },
+
+  _drag : function(event){
+    var newMouse = Rico.eventClient(event);
+    var newLeft = parseInt(this.container.style.left,10) + newMouse.x - this.lastMouse.x;
+    var newTop = parseInt(this.container.style.top,10) + newMouse.y - this.lastMouse.y;
+    this.move(newLeft, newTop);
+    this.lastMouse = newMouse;
+    Rico.eventStop(event);
+  },
+
+  _endDrag : function(){
+    this.container.style.cursor='';
+    Rico.eventUnbind(document, "mousemove", this.dragHandler);
+    Rico.eventUnbind(document, "mouseup", this.dropHandler);
+  },
+
+/**
+ * Display popup at specified position
+ */
+  openPopup: function(left,top) {
+    this.move(left,top);
+    this.container.style.display='';
+    if (this.container.id) Rico.log('openPopup '+this.container.id+' at '+left+','+top);
+    Rico._AddOpenPopup(this);
+  },
+  
+  centerPopup: function() {
+    this.openPopup();
+    var msgWidth=this.container.offsetWidth;
+    var msgHeight=this.container.offsetHeight;
+    var divwi=this.container.parentNode.offsetWidth;
+    var divht=this.container.parentNode.offsetHeight;
+    this.move(parseInt(Math.max((divwi-msgWidth)/2,0),10), parseInt(Math.max((divht-msgHeight)/2,0),10));
+  },
+
+  visible: function() {
+    return Rico.visible(this.container);
+  },
+
+/**
+ * Hide popup
+ */
+  closePopup: function() {
+    Rico._RemoveOpenPopup(this);
+    if (!this.visible()) return;
+    if (this.container.id) Rico.log('closePopup '+this.container.id);
+    if (this.dragEnabled) this._endDrag();
+    this.container.style.display="none";
+    if (this.options.onClose) this.options.onClose();
+  }
+
+};
+
+Rico.closeButton = function(handle) {
+  var a = document.createElement('a');
+  a.className='RicoCloseAnchor';
+  if (Rico.theme.closeAnchor) Rico.addClass(a,Rico.theme.closeAnchor);
+  var span = a.appendChild(document.createElement('span'));
+  span.title=Rico.getPhraseById('close');
+  new Rico.HoverSet([a],{hoverClass: Rico.theme.hover || 'ricoCloseHover'});
+  Rico.addClass(span,Rico.theme.close || 'rico-icon RicoClose');
+  Rico.eventBind(a,"click", handle);
+  return a;
+};
+
+Rico.floatButton = function(buttonName, handle, title) {
+  var a=document.createElement("a");
+  a.className='RicoButtonAnchor'
+  Rico.addClass(a,Rico.theme.buttonAnchor || 'RicoButtonAnchorNative');
+  var span=a.appendChild(document.createElement("span"));
+  if (title) span.title=title;
+  span.className=Rico.theme[buttonName.toLowerCase()] || 'rico-icon Rico'+buttonName;
+  Rico.eventBind(a,"click", handle, false);
+  new Rico.HoverSet([a]);
+  return a
+}
+
+Rico.clearButton = function(handle) {
+  var span=document.createElement("span");
+  span.title=Rico.getPhraseById('clear');
+  span.className='ricoClear';
+  Rico.addClass(span, Rico.theme.clear || 'rico-icon ricoClearNative');
+  Rico.eventBind(span,"click", handle);
+  return span;
+}
+
+Rico.Window = function(title, options, contentParam) {
+  this.initialize(title, options, contentParam);
+};
+
+Rico.Window.prototype = {
+
+/**
+ * Create popup div with a title bar.
+ */
+  initialize: function(title, options, contentParam) {
+    options=options || {overflow:'auto'};
+    Rico.extend(this, new Rico.Popup());
+
+    this.titleDiv = document.createElement('div');
+    this.options.canDrag=true;
+    this.options.dragElement=this.titleDiv;
+    this.createContainer(options);
+    this.content.appendChild(this.titleDiv);
+    contentParam=Rico.$(contentParam);
+    if (contentParam) {
+      this.contentDiv=contentParam
+      contentParam.parentNode.insertBefore(this.container,contentParam);
+    } else {
+      this.contentDiv=document.createElement('div');
+    }
+    this.content.appendChild(this.contentDiv);
+
+    // create title area
+    this.titleDiv.className='ricoTitle';
+    if (Rico.theme.dialogTitle) Rico.addClass(this.titleDiv,Rico.theme.dialogTitle);
+    this.titleDiv.style.position='relative';
+    this.titleContent = document.createElement('span');
+    this.titleContent.className='ricoTitleSpan';
+    this.titleDiv.appendChild(this.titleContent);
+    this.titleDiv.appendChild(Rico.closeButton(Rico.eventHandle(this,'closeFunc')));
+    if (!title && contentParam) {
+      title=contentParam.title;
+      contentParam.title='';
+    }
+    this.setTitle(title || '&nbsp;');
+
+    // create content area
+    this.contentDiv.className='ricoContent';
+    if (Rico.theme.dialogContent) Rico.addClass(this.contentDiv,Rico.theme.dialogContent);
+    this.contentDiv.style.position='relative';
+    if (options.height) this.contentDiv.style.height=options.height;
+    if (options.width) this.contentDiv.style.width=options.width;
+    if (options.overflow) this.contentDiv.style.overflow=options.overflow;
+    Rico.addClass(this.container,'ricoWindow');
+    if (Rico.theme.dialog) Rico.addClass(this.container,Rico.theme.dialog);
+    /*
+    if (Rico.isIE) {
+      // fix float'ed content in IE
+      this.titleDiv.style.zoom=1;
+      this.contentDiv.style.zoom=1;
+    }
+    */
+    this.content=this.contentDiv;
+  },
+
+  setTitle: function(title) {
+    this.titleContent.innerHTML=title;
+  }
+
+}
+
+
+Rico.Menu = function(options) {
+  this.initialize(options);
+}
+
+Rico.Menu.prototype = {
+/**
+ * @class Implements popup menus and submenus
+ * @extends Rico.Popup
+ * @constructs
+ */
+  initialize: function(options) {
+    Rico.extend(this, new Rico.Popup());
+    Rico.extend(this.options, {
+      width        : "15em",
+      arrowColor   : "b",   // for submenus: b=black, w=white
+      showDisabled : false,
+      hideOnClick  : true
+    });
+    if (typeof options=='string')
+      this.options.width=options;
+    else
+      Rico.extend(this.options, options || {});
+    this.hideFunc=null;
+    this.highlightElem=null;
+  },
+
+  createDiv: function(parentNode) {
+    if (this.container) return;
+    var self=this;
+    var options={ closeFunc: function() { self.cancelmenu(); } };
+    if (parentNode) options.parent=parentNode;
+    this.createContainer(options);
+    this.content.className = Rico.isWebKit ? 'ricoMenuSafari' : 'ricoMenu';
+    this.content.style.width=this.options.width;
+    this.direction=Rico.direction(this.container);
+    this.hidemenu();
+    this.itemCount=0;
+  },
+
+  showmenu: function(e,hideFunc){
+    Rico.eventStop(e);
+    this.hideFunc=hideFunc;
+    if (this.content.childNodes.length==0) {
+      this.cancelmenu();
+      return false;
+    }
+    var mousePos = Rico.eventClient(e);
+    this.openmenu(mousePos.x,mousePos.y,0,0);
+  },
+
+  openmenu: function(x,y,clickItemWi,clickItemHt,noOffset) {
+    var newLeft=x + (noOffset ? 0 : Rico.docScrollLeft());
+    this.container.style.visibility="hidden";
+    this.container.style.display="block";
+    var w=this.container.offsetWidth;
+    var cw=this.content.offsetWidth;
+    //window.status='openmenu: newLeft='+newLeft+' width='+w+' clickItemWi='+clickItemWi+' windowWi='+Rico.windowWidth();
+    if (this.direction == 'rtl') {
+      if (newLeft > w+clickItemWi) newLeft-=cw+clickItemWi;
+    } else {
+      if (x+w > Rico.windowWidth()) newLeft-=cw+clickItemWi-2;
+    }
+    var scrTop=Rico.docScrollTop();
+    var newTop=y + (noOffset ? 0 : scrTop);
+    if (y+this.container.offsetHeight-scrTop > Rico.windowHeight())
+      newTop=Math.max(newTop-this.content.offsetHeight+clickItemHt,0);
+    this.openPopup(newLeft,newTop);
+    this.container.style.visibility ="visible";
+    return false;
+  },
+
+  clearMenu: function() {
+    this.clearContent();
+    this.defaultAction=null;
+    this.itemCount=0;
+  },
+
+  addMenuHeading: function(hdg) {
+    var el=document.createElement('div');
+    el.innerHTML=hdg;
+    el.className='ricoMenuHeading';
+    this.content.appendChild(el);
+  },
+
+  addMenuBreak: function() {
+    var brk=document.createElement('div');
+    brk.className="ricoMenuBreak";
+    this.content.appendChild(brk);
+  },
+
+  addSubMenuItem: function(menutext, submenu, translate) {
+    var dir=this.direction=='rtl' ? 'left' : 'right';
+    var a=this.addMenuItem(menutext,null,true,null,translate);
+    a.className='ricoSubMenu';
+    var arrowdiv = a.appendChild(document.createElement('div'));
+    arrowdiv.className='rico-icon rico-'+dir+'-'+this.options.arrowColor;
+    Rico.setStyle(arrowdiv,{position:'absolute',top:'2px'});
+    arrowdiv.style[dir]='0px';
+    a.RicoSubmenu=submenu;
+    Rico.eventBind(a,"mouseover", Rico.eventHandle(this,'showSubMenu'));
+    //Rico.eventBind(a,"mouseout", Rico.eventHandle(this,'subMenuOut'));
+  },
+
+  showSubMenu: function(e) {
+    if (this.openSubMenu) this.hideSubMenu();
+    var a=Rico.eventElement(e);
+    if (!a.RicoSubmenu) a=a.parentNode; // event can happen on arrow div
+    if (!a.RicoSubmenu) return;
+    this.openSubMenu=a.RicoSubmenu;
+    this.openMenuAnchor=a;
+    if (Rico.hasClass(a,'ricoSubMenu')) {
+      Rico.removeClass(a,'ricoSubMenu');
+      Rico.addClass(a,'ricoSubMenuOpen');
+    }
+    a.RicoSubmenu.openmenu(parseInt(this.container.style.left)+a.offsetWidth, parseInt(this.container.style.top)+a.offsetTop, a.offsetWidth-2, a.offsetHeight+2,true);
+  },
+
+  /*
+  subMenuOut: function(e) {
+    if (!this.openSubMenu) return;
+    Rico.eventStop(e);
+    var elem=Rico.eventElement(e);
+    var reltg = Rico.eventRelatedTarget(e) || e.toElement;
+    try {
+      while (reltg != null && reltg != this.openSubMenu.div)
+        reltg=reltg.parentNode;
+    } catch(err) {}
+    if (reltg == this.openSubMenu.div) return;
+    this.hideSubMenu();
+  },
+  */
+
+  hideSubMenu: function() {
+    if (this.openMenuAnchor) {
+      Rico.removeClass(this.openMenuAnchor,'ricoSubMenuOpen');
+      Rico.addClass(this.openMenuAnchor,'ricoSubMenu');
+      this.openMenuAnchor=null;
+    }
+    if (this.openSubMenu) {
+      this.openSubMenu.hidemenu();
+      this.openSubMenu=null;
+    }
+  },
+
+  addMenuItemId: function(phraseId,action,enabled,title,target) {
+    if ( arguments.length < 3 ) enabled=true;
+    this.addMenuItem(Rico.getPhraseById(phraseId),action,enabled,title,target);
+  },
+
+// if action is a string, then it is assumed to be a URL and the target parm can be used indicate which window gets the content
+// action can also be a function
+// action can also be a Rico.eventHandle, but set target='event' in this case
+  addMenuItem: function(menutext,action,enabled,title,target) {
+    if (arguments.length >= 3 && !enabled && !this.options.showDisabled) return null;
+    this.itemCount++;
+    var a = document.createElement(typeof action=='string' ? 'a' : 'div');
+    if ( arguments.length < 3 || enabled ) {
+      if (typeof action=='string') {
+        a.href = action;
+        if (target) a.target = target;
+      } else if (target=='event') {
+        Rico.eventBind(a,"click", action);
+      } else {
+        a.onclick=action;
+      }
+      a.className = 'enabled';
+      if (this.defaultAction==null) this.defaultAction=action;
+    } else {
+      a.disabled = true;
+      a.className = 'disabled';
+    }
+    a.innerHTML = menutext;
+    if (typeof title=='string')
+      a.title = title;
+    a=this.content.appendChild(a);
+    Rico.eventBind(a,"mouseover", Rico.eventHandle(this,'mouseOver'));
+    Rico.eventBind(a,"mouseout", Rico.eventHandle(this,'mouseOut'));
+    return a;
+  },
+
+  mouseOver: function(e) {
+    if (this.highlightElem && this.highlightElem.className=='enabled-hover') {
+      // required for Safari
+      this.highlightElem.className='enabled';
+      this.highlightElem=null;
+    }
+    var elem=Rico.eventElement(e);
+    if (elem.parentNode == this.openMenuAnchor) elem=elem.parentNode;
+    if (this.openMenuAnchor && this.openMenuAnchor!=elem)
+      this.hideSubMenu();
+    if (elem.className=='enabled') {
+      elem.className='enabled-hover';
+      this.highlightElem=elem;
+    }
+  },
+
+  mouseOut: function(e) {
+    var elem=Rico.eventElement(e);
+    if (elem.className=='enabled-hover') elem.className='enabled';
+    if (this.highlightElem==elem) this.highlightElem=null;
+  },
+
+  cancelmenu: function() {
+    if (!this.visible()) return;
+    if (this.hideFunc) this.hideFunc();
+    this.hideFunc=null;
+    this.hidemenu();
+  },
+
+  hidemenu: function() {
+    if (this.openSubMenu) this.openSubMenu.hidemenu();
+    this.closePopup();
+  }
+
+}
+
+
+Rico.SelectionSet = function(selectionSet, options) {
+  this.initialize(selectionSet, options);
+}
+
+Rico.SelectionSet.prototype = {
+/**
+ * @class
+ * @constructs
+ * @param selectionSet collection of DOM elements (or a CSS selection string)
+ * @param options object may contain any of the following:<dl>
+ *   <dt>selectedClass</dt><dd>class name to add when element is selected, default is "selected"</dd>
+ *   <dt>selectNode   </dt><dd>optional function that returns the element to be selected</dd>
+ *   <dt>onSelect     </dt><dd>optional function that gets called when element is selected</dd>
+ *   <dt>onFirstSelect</dt><dd>optional function that gets called the first time element is selected</dd>
+ *   <dt>noDefault    </dt><dd>when true, no element in the set is initially selected, default is false</dd>
+ *   <dt>selectedIndex</dt><dd>index of the element that should be initially selected, default is 0</dd>
+ *   <dt>cookieName   </dt><dd>optional name of cookie to use to remember selected element. If specified, and the cookie exists, then the cookie value overrides selectedIndex.</dd>
+ *   <dt>cookieDays   </dt><dd>specifies how long cookie should persist (in days). If unspecified, then the cookie persists for the current session.</dd>
+ *   <dt>cookiePath   </dt><dd>optional cookie path</dd>
+ *   <dt>cookieDomain </dt><dd>optional cookie domain</dd>
+ *</dl>
+ */
+  initialize: function(selectionSet, options){
+    Rico.log('SelectionSet#initialize');
+    this.options = options || {};
+    if (typeof selectionSet == 'string')
+      selectionSet = Rico.select(selectionSet);
+    this.previouslySelected = [];
+    this.selectionSet = [];
+    this.selectedClassName = this.options.selectedClass || Rico.theme.selected || "selected";
+    this.selectNode = this.options.selectNode || function(e){return e;};
+    this.onSelect = this.options.onSelect;
+    this.onFirstSelect = this.options.onFirstSelect;
+    var self=this;
+    this.clickHandler = function(idx) { self.selectIndex(idx); };
+    this.selectedIndex=-1;
+    for (var i=0; i<selectionSet.length; i++)
+      this.add(selectionSet[i]);
+    if (!this.options.noDefault) {
+      var cookieIndex=this.options.cookieName ? this.getCookie() : 0;
+      this.selectIndex(cookieIndex || this.options.selectedIndex || 0);
+    }
+  },
+  getCookie: function() {
+    var cookie = Rico.getCookie(this.options.cookieName);
+    if (!cookie) return 0;
+    var index = parseInt(cookie);
+    return index < this.selectionSet.length ? index : 0;
+  },
+  reset: function(){
+    this.previouslySelected = [];
+    this._notifySelected(this.selectedIndex);
+  },
+  clearSelected: function() {
+    if (this.selected)
+      Rico.removeClass(this.selectNode(this.selected), this.selectedClassName);
+  },
+  getIndex: function(element) {
+    for (var i=0; i<this.selectionSet.length; i++) {
+      if (element == this.selectionSet[i]) return i;
+    }
+    return -1;
+  },
+  select: function(element){
+    if (this.selected == element) return;
+    var i=this.getIndex(element);
+    if (i >= 0) this.selectIndex(i);
+  },
+  _notifySelected: function(index){
+    if (index < 0) return;
+    var element = this.selectionSet[index];
+    if (this.options.cookieName)
+      Rico.setCookie(this.options.cookieName, index, this.options.cookieDays, this.options.cookiePath, this.options.cookieDomain);
+    if (this.onFirstSelect && !this.previouslySelected[index]){
+      this.onFirstSelect(element, index);
+      this.previouslySelected[index] = true;
+    }
+    if (this.onSelect)
+      try{
+        this.onSelect(index);
+      } catch (e) {};
+  },
+  selectIndex: function(index){
+    if (this.selectedIndex == index || index >= this.selectionSet.length) return;
+    this.clearSelected();
+    this._notifySelected(index);
+    this.selectedIndex = index;
+    this.selected=this.selectionSet[index].element;
+    Rico.addClass(this.selectNode(this.selected), this.selectedClassName);
+  },
+  nextSelectIndex: function(){
+    return (this.selectedIndex + 1) % this.selectionSet.length;
+  },
+  nextSelectItem: function(){
+    return this.selectionSet[this.nextSelectIndex()];
+  },
+  selectNext: function(){
+    this.selectIndex(this.nextSelectIndex());
+  },
+  add: function(item){
+    var index=this.selectionSet.length;
+    this.selectionSet[index] = new Rico._SelectionItem(item,index,this.clickHandler);
+  },
+  remove: function(item){
+    if (item==this.selected) this.clearSelected();
+    var i=this.getIndex(item);
+    if (i < 0) return;
+    this.selectionSet[i].remove();
+    this.selectionSet.splice(i,1);
+  },
+  removeAll: function(){
+    this.clearSelected();
+    while (this.selectionSet.length > 0) {
+      this.selectionSet.pop().remove();
+    }
+  }
+};
+
+
+Rico._SelectionItem=function(element,index,callback) {
+  this.add(element,index,callback);
+};
+
+Rico._SelectionItem.prototype = {
+  add: function(element,index,callback) {
+    this.element=element;
+    this.index=index;
+    this.callback=callback;
+    this.handle=Rico.eventHandle(this,'click');
+    Rico.eventBind(element, "click", this.handle);
+  },
+
+  click: function(ev) {
+    this.callback(this.index);
+  },
+
+  remove: function() {
+    Rico.eventUnbind(this.element, "click", this.handle);
+  }
+};
+
+
+Rico.HoverSet = function(hoverSet, options) {
+  this.initialize(hoverSet, options);
+};
+
+Rico.HoverSet.prototype = {
+/**
+ * @class
+ * @constructs
+ * @param hoverSet collection of DOM elements
+ * @param options object may contain any of the following:<dl>
+ *   <dt>hoverClass</dt><dd> class name to add when mouse is over element, default is "hover"</dd>
+ *   <dt>hoverNodes</dt><dd> optional function to select/filter which nodes are in the set</dd>
+ *</dl>
+ */
+  initialize: function(hoverSet, options){
+    Rico.log('HoverSet#initialize');
+    options = options || {};
+    this.hoverClass = options.hoverClass || Rico.theme.hover || "hover";
+    this.hoverFunc = options.hoverNodes || function(e){return [e];};
+    this.hoverSet=[];
+    if (!hoverSet) return;
+    for (var i=0; i<hoverSet.length; i++)
+      this.add(hoverSet[i]);
+  },
+  add: function(item) {
+    this.hoverSet.push(new Rico._HoverItem(item,this.hoverFunc,this.hoverClass));
+  },
+  removeAll: function(){
+    while (this.hoverSet.length > 0) {
+      this.hoverSet.pop().remove();
+    }
+  }
+};
+
+
+Rico._HoverItem=function(element,selectFunc,hoverClass) {
+  this.add(element,selectFunc,hoverClass);
+};
+
+Rico._HoverItem.prototype = {
+  add: function(element,selectFunc,hoverClass) {
+    this.element=element;
+    this.selectFunc=selectFunc;
+    this.hoverClass=hoverClass;
+    this.movehandle=Rico.eventHandle(this,'move');
+    this.outhandle=Rico.eventHandle(this,'mouseout');
+    Rico.eventBind(element, "mousemove", this.movehandle);
+    Rico.eventBind(element, "mouseout", this.outhandle);
+  },
+
+  move: function(ev) {
+    var elems=this.selectFunc(this.element);
+    for (var i=0; i<elems.length; i++)
+      Rico.addClass(elems[i],this.hoverClass);
+  },
+
+  mouseout: function(ev) {
+    var elems=this.selectFunc(this.element);
+    for (var i=0; i<elems.length; i++)
+      Rico.removeClass(elems[i],this.hoverClass);
+  },
+
+  remove: function() {
+    Rico.eventUnbind(element, "mousemove", this.movehandle);
+    Rico.eventUnbind(element, "mouseout", this.outhandle);
+  }
+};
+
+
+/** @class core methods for transition effects */
+Rico.ContentTransitionBase = function() {};
+Rico.ContentTransitionBase.prototype = {
+  initBase: function(titles, contents, options) {
+    this.options = options || {};
+    this.titles = titles;
+    this.contents = contents;
+    this.hoverSet = new Rico.HoverSet(titles, options);
+    for (var i=0; i<contents.length; i++) {
+      if (contents[i]) Rico.hide(contents[i]);
+    }
+    var self=this;
+    this.selectionSet = new Rico.SelectionSet(titles, Rico.extend(options, { onSelect: function(idx) { self._finishSelect(idx); } }));
+  },
+  reset: function(){
+    this.selectionSet.reset();
+  },
+  select: function(index) {
+    this.selectionSet.selectIndex(index);
+  },
+  _finishSelect: function(index) {
+    Rico.log('ContentTransitionBase#_finishSelect');
+    var panel = this.contents[index];
+    if (!panel) {
+      alert('Internal error: no panel @index='+index);
+      return;
+    }
+    if ( this.selected == panel) return;
+    if (this.transition){
+      if (this.selected){
+        this.transition(panel);
+      } else {
+        panel.style.display='block';
+      }
+    } else {
+      if (this.selected) Rico.hide(this.selected);
+      panel.style.display='block';
+    }
+    this.selected = panel;
+  },
+  addBase: function(title, content){
+    this.titles.push(title);
+    this.contents.push(content);
+    this.hoverSet.add(title);
+    this.selectionSet.add(title);
+    Rico.hide(content);
+    //this.selectionSet.select(title);
+  },
+  removeAll: function(){
+    this.hoverSet.removeAll();
+    this.selectionSet.removeAll();
+  }
+};
+
+
+/**
+ * @class Implements accordion effect
+ * @see Rico.ContentTransitionBase#initialize for construction parameters
+ * @extends Rico.ContentTransitionBase
+ */
+Rico.Accordion = function(element, options) {
+  this.initialize(element, options);
+};
+
+Rico.Accordion.prototype = Rico.extend(new Rico.ContentTransitionBase(),
+/** @lends Rico.Accordion# */
+{
+  initialize: function(element, options) {
+    element=Rico.$(element);
+    element.style.overflow='hidden';
+    element.className=options.accClass || Rico.theme.accordion || "Rico_accordion";
+    if (typeof options.panelWidth=='number') options.panelWidth+="px";
+    if (options.panelWidth) element.style.width = options.panelWidth;
+    var panels=Rico.getDirectChildrenByTag(element,'div');
+    var items,titles=[], contents=[];
+    for (var i=0; i<panels.length; i++) {
+      items=Rico.getDirectChildrenByTag(panels[i],'div');
+      if (items.length>=2) {
+        items[0].className=options.titleClass || Rico.theme.accTitle || "Rico_accTitle";
+        items[1].className=options.contentClass || Rico.theme.accContent || "Rico_accContent";
+        titles.push(items[0]);
+        contents.push(items[1]);
+        var a=Rico.wrapChildren(items[0],'','','a');
+        a.href="javascript:void(0)";
+      }
+    }
+    Rico.log('creating Rico.Accordion for '+element.id+' with '+titles.length+' panels');
+    this.initBase(titles, contents, options);
+    this.selected.style.height = this.options.panelHeight + "px";
+    this.totSteps=(typeof options.duration =='number' ? options.duration : 200)/25;
+  },
+  transition: function(p){
+    if (!this.options.noAnimate) {
+      this.closing=this.selected;
+      this.opening=p;
+      this.curStep=0;
+      var self=this;
+      this.timer=setInterval(function() { self.step(); },25);
+    } else {
+      p.style.height = this.options.panelHeight + "px";
+      if (this.selected) Rico.hide(this.selected);
+      p.style.display='block';
+    }
+  },
+  step: function() {
+    this.curStep++;
+    var oheight=Math.round(this.curStep/this.totSteps*this.options.panelHeight);
+    this.opening.style.height=oheight+'px';
+    this.closing.style.height=(this.options.panelHeight - oheight)+'px';
+    if (this.curStep==1) {
+      this.opening.style.paddingTop=this.opening.style.paddingBottom='0px';
+      this.opening.style.display='block';
+    }
+    if (this.curStep==this.totSteps) {
+      clearInterval(this.timer);
+      this.opening.style.paddingTop=this.opening.style.paddingBottom='';
+      Rico.hide(this.closing);
+    }
+  },
+  setPanelHeight: function(h) {
+    this.options.panelHeight = h;
+    this.selected.style.height = this.options.panelHeight + "px";
+  }
+});
+
+
+/**
+ * @class Implements tabbed panel effect
+ * @see Rico.ContentTransitionBase#initialize for construction parameters
+ * @extends Rico.ContentTransitionBase
+ */
+Rico.TabbedPanel = function(element, options) {
+  this.initialize(element, options);
+};
+
+Rico.TabbedPanel.prototype = Rico.extend(new Rico.ContentTransitionBase(),
+{
+  initialize: function(element, options) {
+    element=Rico.$(element);
+    options=options || {};
+    if (typeof options.panelWidth=='number') options.panelWidth+="px";
+    if (typeof options.panelHeight=='number') options.panelHeight+="px";
+    element.className=options.tabClass || Rico.theme.tabPanel || "Rico_tabPanel";
+    if (options.panelWidth) element.style.width = options.panelWidth;
+    var items = [];
+    var allKids = element.childNodes;
+    for( var i = 0 ; i < allKids.length ; i++ ) {
+      if (allKids[i] && allKids[i].tagName && allKids[i].tagName.match(/^div|ul$/i))
+        items.push(allKids[i]);
+    }
+    if (items.length < 2) return;
+    var childTag=items[0].tagName.toLowerCase()=='ul' ? 'li' : 'div';
+    items[0].className=options.navContainerClass || Rico.theme.tabNavContainer || "Rico_tabNavContainer";
+    items[0].style.listStyle='none';
+    items[1].className=options.contentContainerClass || Rico.theme.tabContentContainer || "Rico_tabContentContainer";
+    var titles=Rico.getDirectChildrenByTag(items[0], childTag);
+    var contents=Rico.getDirectChildrenByTag(items[1],'div');
+    var direction=Rico.direction(element);
+    if (!options.corners) options.corners='top';
+    for (var i=0; i<titles.length; i++) {
+      if (direction == 'rtl') Rico.setStyle(titles[i], {'float':'right'});
+      titles[i].className=options.titleClass || Rico.theme.tabTitle || "Rico_tabTitle";
+      var a=Rico.wrapChildren(titles[i],'','','a');
+      a.href="javascript:void(0)";
+      contents[i].className=options.contentClass || Rico.theme.tabContent || "Rico_tabContent";
+      if (options.panelHeight) contents[i].style.overflow='auto';
+      if (options.corners!='none') {
+        if (options.panelHdrWidth) titles[i].style.width=options.panelHdrWidth;
+        Rico.Corner.round(titles[i], Rico.theme.tabCornerOptions || options);
+      }
+    }
+    options.selectedClass=Rico.theme.tabSelected || 'selected';
+    this.initBase(titles, contents, options);
+    if (this.selected) this.transition(this.selected);
+  },
+  transition: function(p){
+    Rico.log('TabbedPanel#transition '+typeof(p));
+    if (this.selected) Rico.hide(this.selected);
+    Rico.show(p);
+    if (this.options.panelHeight) p.style.height = this.options.panelHeight;
+  }
+});
+
+
+/**
+ * @namespace
+ */
+Rico.Corner = {
+
+   round: function(e, options) {
+      e = Rico.$(e);
+      this.options = {
+         corners : "all",
+         bgColor : "fromParent",
+         compact : false,
+         nativeCorners: false  // only use native corners?
+      };
+      Rico.extend(this.options, options || {});
+      if (typeof(Rico.getStyle(e,'border-radius'))=='string')
+        this._roundCornersStdCss(e);
+      else if (typeof(Rico.getStyle(e,'-webkit-border-radius'))=='string')
+        this._roundCornersWebKit(e);
+      else if (typeof(Rico.getStyle(e,'-moz-border-radius'))=='string')
+        this._roundCornersMoz(e);
+      else if (!this.options.nativeCorners)
+        this._roundCornersImpl(e);
+   },
+
+    _roundCornersStdCss: function(e) {
+      var radius=this.options.compact ? '4px' : '8px';
+      if (this._hasString(this.options.corners, "all"))
+        Rico.setStyle(e, {borderRadius:radius});
+      else {
+        if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {borderTopLeftRadius:radius});
+        if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {borderTopRightRadius:radius});
+        if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {borderBottomLeftRadius:radius});
+        if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {borderBottomRightRadius:radius});
+      }
+   },
+
+   _roundCornersWebKit: function(e) {
+      var radius=this.options.compact ? '4px' : '8px';
+      if (this._hasString(this.options.corners, "all"))
+        Rico.setStyle(e, {WebkitBorderRadius:radius});
+      else {
+        if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {WebkitBorderTopLeftRadius:radius});
+        if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {WebkitBorderTopRightRadius:radius});
+        if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {WebkitBorderBottomLeftRadius:radius});
+        if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {WebkitBorderBottomRightRadius:radius});
+      }
+   },
+
+   _roundCornersMoz: function(e) {
+      var radius=this.options.compact ? '4px' : '8px';
+      if (this._hasString(this.options.corners, "all"))
+        Rico.setStyle(e, {MozBorderRadius:radius});
+      else {
+        if (this._hasString(this.options.corners, "top", "tl")) Rico.setStyle(e, {MozBorderRadiusTopleft:radius});
+        if (this._hasString(this.options.corners, "top", "tr")) Rico.setStyle(e, {MozBorderRadiusTopright:radius});
+        if (this._hasString(this.options.corners, "bottom", "bl")) Rico.setStyle(e, {MozBorderRadiusBottomleft:radius});
+        if (this._hasString(this.options.corners, "bottom", "br")) Rico.setStyle(e, {MozBorderRadiusBottomright:radius});
+      }
+   },
+
+  _roundCornersImpl: function(e) {
+      var bgColor = this.options.bgColor == "fromParent" ? this._background(e.parentNode) : this.options.bgColor;
+      e.style.position='relative';
+      //this.options.numSlices = this.options.compact ? 2 : 4;
+      if (this._hasString(this.options.corners, "all", "top", "tl")) this._createCorner(e,'top','left',bgColor);
+      if (this._hasString(this.options.corners, "all", "top", "tr")) this._createCorner(e,'top','right',bgColor);
+      if (this._hasString(this.options.corners, "all", "bottom", "bl")) this._createCorner(e,'bottom','left',bgColor);
+      if (this._hasString(this.options.corners, "all", "bottom", "br")) this._createCorner(e,'bottom','right',bgColor);
+   },
+
+  _createCorner: function(elem,tb,lr,bgColor) {
+    //alert('Corner: '+tb+' '+lr+' bgColor='+typeof(bgColor));
+    var corner = document.createElement("div");
+    corner.className='ricoCorner';
+    Rico.setStyle(corner,{width:'6px', height:'5px'});
+    var borderStyle = Rico.getStyle(elem,'border-'+tb+'-style');
+    var borderColor = borderStyle=='none' ? bgColor : Rico.getStyle(elem,'border-'+tb+'-color');
+    //alert('Corner: '+tb+' '+borderStyle+borderColor+' '+);
+    var pos=borderStyle=='none' ? '0px' : '-1px';
+    corner.style[tb]=pos;
+    corner.style[lr]=Rico.isIE && Rico.ieVersion<7 && lr=='right' && borderStyle!='none' ? '-2px' : '-1px';
+    //corner.style[lr]='-1px';
+    elem.appendChild(corner);
+    var marginSizes = [ 0, 2, 3, 4, 4 ];
+    if (tb=='bottom') marginSizes.reverse();
+    var borderVal= borderStyle=='none' ? '0px none' : '1px solid '+borderColor;
+    var d= lr=='left' ? 'Right' : 'Left';
+    for (var i=0; i<marginSizes.length; i++) {
+      var slice = document.createElement("div");
+      Rico.setStyle(slice,{backgroundColor:bgColor,height:'1px'});
+      slice.style['margin'+d]=marginSizes[i]+'px';
+      slice.style['border'+d]=borderVal;
+      corner.appendChild(slice);
+    }
+  },
+
+  _background: function(elem) {
+     try {
+       var actualColor = Rico.getStyle(elem, "backgroundColor");
+
+       // if color is tranparent, check parent
+       // Safari returns "rgba(0, 0, 0, 0)", which means transparent
+       if ( actualColor.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i) && elem.parentNode )
+          return this._background(elem.parentNode);
+
+       return actualColor == null ? "#ffffff" : actualColor;
+     } catch(err) {
+       return "#ffffff";
+     }
+   },
+
+   _hasString: function(str) {
+     for(var i=1 ; i<arguments.length ; i++) {
+       if (str.indexOf(arguments[i]) >= 0) return true;
+     }
+     return false;
+   }
+
+};
+
+Rico.toColorPart = function(c) {
+  return Rico.zFill(c, 2, 16);
+};
+
+
+Rico.Color = function(red, green, blue) {
+  this.initialize(red, green, blue);
+};
+
+Rico.Color.prototype = {
+/**
+ * @class Methods to manipulate color values.
+ * @constructs
+ * @param red integer (0-255)
+ * @param green integer (0-255)
+ * @param blue integer (0-255)
+ */
+   initialize: function(red, green, blue) {
+      this.rgb = { r: red, g : green, b : blue };
+   },
+
+   setRed: function(r) {
+      this.rgb.r = r;
+   },
+
+   setGreen: function(g) {
+      this.rgb.g = g;
+   },
+
+   setBlue: function(b) {
+      this.rgb.b = b;
+   },
+
+   setHue: function(h) {
+
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.h = h;
+
+      // convert back to RGB...
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
+   },
+
+   setSaturation: function(s) {
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.s = s;
+
+      // convert back to RGB and set values...
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
+   },
+
+   setBrightness: function(b) {
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.b = b;
+
+      // convert back to RGB and set values...
+      this.rgb = Rico.Color.HSBtoRGB( hsb.h, hsb.s, hsb.b );
+   },
+
+   darken: function(percent) {
+      var hsb  = this.asHSB();
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.max(hsb.b - percent,0));
+   },
+
+   brighten: function(percent) {
+      var hsb  = this.asHSB();
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.min(hsb.b + percent,1));
+   },
+
+   blend: function(other) {
+      this.rgb.r = Math.floor((this.rgb.r + other.rgb.r)/2);
+      this.rgb.g = Math.floor((this.rgb.g + other.rgb.g)/2);
+      this.rgb.b = Math.floor((this.rgb.b + other.rgb.b)/2);
+   },
+
+   isBright: function() {
+      var hsb = this.asHSB();
+      return this.asHSB().b > 0.5;
+   },
+
+   isDark: function() {
+      return ! this.isBright();
+   },
+
+   asRGB: function() {
+      return "rgb(" + this.rgb.r + "," + this.rgb.g + "," + this.rgb.b + ")";
+   },
+
+   asHex: function() {
+      return "#" + Rico.toColorPart(this.rgb.r) + Rico.toColorPart(this.rgb.g) + Rico.toColorPart(this.rgb.b);
+   },
+
+   asHSB: function() {
+      return Rico.Color.RGBtoHSB(this.rgb.r, this.rgb.g, this.rgb.b);
+   },
+
+   toString: function() {
+      return this.asHex();
+   }
+
+};
+
+/**
+ * Factory method for creating a color from an RGB string
+ * @param hexCode a 3 or 6 digit hex string, optionally preceded by a # symbol
+ * @returns a Rico.Color object
+ */
+Rico.Color.createFromHex = function(hexCode) {
+  if(hexCode.length==4) {
+    var shortHexCode = hexCode;
+    hexCode = '#';
+    for(var i=1;i<4;i++)
+      hexCode += (shortHexCode.charAt(i) + shortHexCode.charAt(i));
+  }
+  if ( hexCode.indexOf('#') == 0 )
+    hexCode = hexCode.substring(1);
+  if (!hexCode.match(/^[0-9A-Fa-f]{6}$/)) return null;
+  var red   = hexCode.substring(0,2);
+  var green = hexCode.substring(2,4);
+  var blue  = hexCode.substring(4,6);
+  return new Rico.Color( parseInt(red,16), parseInt(green,16), parseInt(blue,16) );
+};
+
+/**
+ * Retrieves the background color of an HTML element
+ * @param elem the DOM element whose background color should be retreived
+ * @returns a Rico.Color object
+ */
+Rico.Color.createColorFromBackground = function(elem) {
+
+   if (!elem.style) return new Rico.Color(255,255,255);
+   var actualColor = Rico.getStyle(elem, "background-color");
+
+   // if color is tranparent, check parent
+   // Safari returns "rgba(0, 0, 0, 0)", which means transparent
+   if ( actualColor.match(/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/i) && elem.parentNode )
+      return Rico.Color.createColorFromBackground(elem.parentNode);
+
+   if (actualColor == null) return new Rico.Color(255,255,255);
+
+   if ( actualColor.indexOf("rgb(") == 0 ) {
+      var colors = actualColor.substring(4, actualColor.length - 1 );
+      var colorArray = colors.split(",");
+      return new Rico.Color( parseInt( colorArray[0],10 ),
+                             parseInt( colorArray[1],10 ),
+                             parseInt( colorArray[2],10 )  );
+
+   }
+   else if ( actualColor.indexOf("#") == 0 ) {
+      return Rico.Color.createFromHex(actualColor);
+   }
+   else
+      return new Rico.Color(255,255,255);
+};
+
+/**
+ * Converts hue/saturation/brightness to RGB
+ * @returns a 3-element object: r=red, g=green, b=blue.
+ */
+Rico.Color.HSBtoRGB = function(hue, saturation, brightness) {
+
+  var red   = 0;
+  var green = 0;
+  var blue  = 0;
+
+  if (saturation == 0) {
+     red = parseInt(brightness * 255.0 + 0.5,10);
+     green = red;
+     blue = red;
+  }
+  else {
+      var h = (hue - Math.floor(hue)) * 6.0;
+      var f = h - Math.floor(h);
+      var p = brightness * (1.0 - saturation);
+      var q = brightness * (1.0 - saturation * f);
+      var t = brightness * (1.0 - (saturation * (1.0 - f)));
+
+      switch (parseInt(h,10)) {
+         case 0:
+            red   = (brightness * 255.0 + 0.5);
+            green = (t * 255.0 + 0.5);
+            blue  = (p * 255.0 + 0.5);
+            break;
+         case 1:
+            red   = (q * 255.0 + 0.5);
+            green = (brightness * 255.0 + 0.5);
+            blue  = (p * 255.0 + 0.5);
+            break;
+         case 2:
+            red   = (p * 255.0 + 0.5);
+            green = (brightness * 255.0 + 0.5);
+            blue  = (t * 255.0 + 0.5);
+            break;
+         case 3:
+            red   = (p * 255.0 + 0.5);
+            green = (q * 255.0 + 0.5);
+            blue  = (brightness * 255.0 + 0.5);
+            break;
+         case 4:
+            red   = (t * 255.0 + 0.5);
+            green = (p * 255.0 + 0.5);
+            blue  = (brightness * 255.0 + 0.5);
+            break;
+          case 5:
+            red   = (brightness * 255.0 + 0.5);
+            green = (p * 255.0 + 0.5);
+            blue  = (q * 255.0 + 0.5);
+            break;
+      }
+  }
+
+   return { r : parseInt(red,10), g : parseInt(green,10) , b : parseInt(blue,10) };
+};
+
+/**
+ * Converts RGB value to hue/saturation/brightness
+ * @param r integer (0-255)
+ * @param g integer (0-255)
+ * @param b integer (0-255)
+ * @returns a 3-element object: h=hue, s=saturation, b=brightness.
+ * (unlike some HSB documentation which states hue should be a value 0-360, this routine returns hue values from 0 to 1.0)
+ */
+Rico.Color.RGBtoHSB = function(r, g, b) {
+
+   var hue;
+   var saturation;
+   var brightness;
+
+   var cmax = (r > g) ? r : g;
+   if (b > cmax)
+      cmax = b;
+
+   var cmin = (r < g) ? r : g;
+   if (b < cmin)
+      cmin = b;
+
+   brightness = cmax / 255.0;
+   if (cmax != 0)
+      saturation = (cmax - cmin)/cmax;
+   else
+      saturation = 0;
+
+   if (saturation == 0)
+      hue = 0;
+   else {
+      var redc   = (cmax - r)/(cmax - cmin);
+      var greenc = (cmax - g)/(cmax - cmin);
+      var bluec  = (cmax - b)/(cmax - cmin);
+
+      if (r == cmax)
+         hue = bluec - greenc;
+      else if (g == cmax)
+         hue = 2.0 + redc - bluec;
+      else
+         hue = 4.0 + greenc - redc;
+
+      hue = hue / 6.0;
+      if (hue < 0)
+         hue = hue + 1.0;
+   }
+
+   return { h : hue, s : saturation, b : brightness };
+};
+
+/**
+ * Returns a new XML document object
+ */
+Rico.createXmlDocument = function() {
+  if (document.implementation && document.implementation.createDocument) {
+    var doc = document.implementation.createDocument("", "", null);
+    // some older versions of Moz did not support the readyState property
+    // and the onreadystate event so we patch it!
+    if (doc.readyState == null) {
+      doc.readyState = 1;
+      doc.addEventListener("load", function () {
+        doc.readyState = 4;
+        if (typeof doc.onreadystatechange == "function") {
+          doc.onreadystatechange();
+        }
+      }, false);
+    }
+    return doc;
+  }
+
+  if (window.ActiveXObject)
+      return Rico.tryFunctions(
+        function() { return new ActiveXObject('MSXML2.DomDocument');   },
+        function() { return new ActiveXObject('Microsoft.DomDocument');},
+        function() { return new ActiveXObject('MSXML.DomDocument');    },
+        function() { return new ActiveXObject('MSXML3.DomDocument');   }
+      ) || false;
+  return null;
+}
diff --git a/lib/rico3/plugins/php/ricoResponse.php b/lib/rico3/plugins/php/ricoResponse.php
new file mode 100644 (file)
index 0000000..b85121b
--- /dev/null
@@ -0,0 +1,657 @@
+<?php
+
+// for PHP4
+// copied from http://www.php.net/json_encode
+if (!function_exists('json_encode'))
+{
+  function json_encode($a=false)
+  {
+    if (is_null($a)) return 'null';
+    if ($a === false) return 'false';
+    if ($a === true) return 'true';
+    if (is_scalar($a))
+    {
+      if (is_float($a))
+      {
+        // Always use "." for floats.
+        return floatval(str_replace(",", ".", strval($a)));
+      }
+
+      if (is_string($a))
+      {
+        static $jsonReplaces = array(array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"'), array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"'));
+        return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"';
+      }
+      else
+        return $a;
+    }
+    $isList = true;
+    for ($i = 0, reset($a); $i < count($a); $i++, next($a))
+    {
+      if (key($a) !== $i)
+      {
+        $isList = false;
+        break;
+      }
+    }
+    $result = array();
+    if ($isList)
+    {
+      foreach ($a as $v) $result[] = json_encode($v);
+      return '[' . join(',', $result) . ']';
+    }
+    else
+    {
+      foreach ($a as $k => $v) $result[] = json_encode($k).':'.json_encode($v);
+      return '{' . join(',', $result) . '}';
+    }
+  }
+}
+
+class ricoXmlResponse {
+
+  // public properties
+  var $orderByRef;
+  var $sendDebugMsgs;
+  var $readAllRows;    // always return the total number of rows? (if true, the user will always see the total number of rows, but there is a small performance hit)
+  var $convertCharSet; // set to true if database is ISO-8859-1 encoded, false if UTF-8
+  var $AllRowsMax;     // max # of rows to send if numrows=-1
+  var $fmt;            // xml, json, html, xl
+  // private properties
+  var $objDB;
+  var $eof;
+  var $oParse;
+  var $sqltext;
+  var $arParams;
+  var $allParams;
+  var $condType;
+  var $RowsStart;
+  var $RowsEnd;
+  var $SendHdg;
+  var $Headings;
+  var $HiddenCols;
+  var $gridDefinition;
+
+  function ricoXmlResponse() {
+    if (isset($GLOBALS['oDB']) && is_object($GLOBALS['oDB'])) {
+      $this->objDB=$GLOBALS['oDB'];   // use oDB global as database connection, if it exists
+    }
+    $this->orderByRef=false;
+    $this->sendDebugMsgs=false;
+    $this->readAllRows=true;    // has no effect on SQL Server 2005, Oracle, and MySQL because they use Query2xmlRaw_Limit()
+    $this->convertCharSet=false;
+    $this->SendHdg=false;
+    $this->AllRowsMax=1999;
+    $this->Headings=array();
+    $this->HiddenCols=array();
+  }
+
+  function ProcessQuery($id, $sqlselect, $filters=array(), $errmsg='',$sqldistinctselect=false) {
+    $this->fmt=isset($_GET["_fmt"]) ? $_GET["_fmt"] : "xml";
+    $offset=isset($_GET["offset"]) ? $_GET["offset"] : "0";
+    $size=isset($_GET["page_size"]) ? $_GET["page_size"] : "";
+    $total=isset($_GET["get_total"]) ? strtolower($_GET["get_total"]) : "false";
+    $distinct=isset($_GET["distinct"]) ? $_GET["distinct"] : "";
+    $edit=isset($_GET["edit"]) ? $_GET["edit"] : "";
+    if (isset($_GET["hidden"]) && $_GET["hidden"]!="") $this->HiddenCols=explode(",", $_GET["hidden"]);
+
+    ob_clean();
+    if ($this->fmt != "xl") {
+      header("Cache-Control: no-cache");
+      header("Pragma: no-cache");
+      header("Expires: ".gmdate("D, d M Y H:i:s",time()+(-1*60))." GMT");
+    }
+
+    switch ($this->fmt) {
+      case "html":
+        header("Content-type: text/html");
+        echo "<html><head></head><body>\n";
+        $closetags="</body></html>";
+        $this->RowsStart="\n<table border='1'>";
+        $this->RowsEnd="\n</table>";
+        $total="false";
+        $this->sendDebugMsgs=false;
+        $this->SendHdg=true;
+        break;
+      case "xl":
+        $this->convertCharSet=false;
+        header("Content-type: application/vnd.ms-excel");
+        echo "<html><head></head><body>\n";
+        $closetags="</body></html>";
+        $this->RowsStart="\n<table>";
+        $this->RowsEnd="\n</table>";
+        $total="false";
+        $this->sendDebugMsgs=false;
+        $this->SendHdg=true;
+        break;
+      case "json":
+        header("Content-type: application/json");
+        echo "{\n\"id\":\"" . $id . "\"";
+        $this->RowsStart=",\n\"update_ui\":true,\n\"offset\":" . $offset . ",\n\"rows\":[";
+        $this->RowsEnd="\n]";
+        $closetags="}";
+        $this->sendDebugMsgs=false;
+        break;
+      default:
+        // default to xml
+        $this->fmt="xml";
+        header("Content-type: text/xml");
+        echo "<?xml version='1.0' encoding='UTF-8'?".">\n";
+        echo "\n<ajax-response><response type='object' id='" . $id . "'>";
+        $closetags="</response></ajax-response>";
+        $this->RowsStart="\n<rows update_ui='true' offset='" . $offset . "'>";
+        $this->RowsEnd="\n</rows>";
+        break;
+    }
+    if (!empty($errmsg)) {
+      $this->ErrorResponse($errmsg);
+    } elseif (empty($id)) {
+      $this->ErrorResponse("No ID provided!");
+    } elseif ($distinct=="" && !is_numeric($offset)) {
+      $this->ErrorResponse("Invalid offset!");
+    } elseif ($distinct=="" && !is_numeric($size)) {
+      $this->ErrorResponse("Invalid size!");
+    } elseif ($distinct!="" && !is_numeric($distinct)) {
+      $this->ErrorResponse("Invalid distinct parameter!");
+    } else {
+      if ($this->SendHdg && is_array($sqlselect)) {
+        // populate $Headings from $sqlselect[9] taking into account hidden columns
+        for ($i=0,$j=0,$SkipIdx=0; $i<count($sqlselect[9]); $i++) {
+          $skip=false;
+          if ($SkipIdx < count($this->HiddenCols)) {
+            $skip=($this->HiddenCols[$SkipIdx] == $i);
+            if ($skip) $SkipIdx++;
+          }
+          if (!$skip) {
+            $this->Headings[$j++]=$sqlselect[9][$i];
+          }
+        }
+      }
+      $this->objDB->DisplayErrors=false;
+      $this->objDB->ErrMsgFmt="MULTILINE";
+      if ($distinct!="" && is_numeric($distinct)) {
+        $this->Query2xmlDistinct($sqlselect, intval($distinct), -1, $filters, $sqldistinctselect);
+      } elseif ($edit!="" && is_numeric($edit) && is_array($sqlselect)) {
+        $this->Query2xml($sqlselect[8][intval($edit)], intval($offset), intval($size), $total!="false", $filters);
+      } else {
+        $this->Query2xml($sqlselect, intval($offset), intval($size), $total!="false", $filters);
+      }
+      if (!empty($this->objDB->LastErrorMsg)) {
+        $this->ErrorResponse($this->objDB->LastErrorMsg);
+      }
+    }
+    echo "\n".$closetags;
+  }
+
+  function ErrorResponse($msg) {
+    $this->AppendResponse("error",$msg);
+  }
+
+  function AppendResponse($tag, $content) {
+    switch ($this->fmt) {
+      case "html":
+        echo "\n<p>".$tag."<br>".htmlspecialchars($content)."</p>";
+        break;
+      case "xl":
+        echo "\n<p>".$tag."<br>".htmlspecialchars($content)."</p>";
+        break;
+      case "json":
+        echo ",\n\"".$tag."\":".json_encode($content);
+        break;
+      case "xml":
+        echo "\n<".$tag.">".htmlspecialchars($content)."</".$tag.">";
+        break;
+    }
+  }
+
+  // All Oracle and SQL Server 2005 queries *must* have an ORDER BY clause
+  // "as" clauses are now ok
+  // If numrows < 0, then retrieve all rows
+  function Query2xml($sqlselect, $offset, $numrows, $gettotal, $filters=array()) {
+    if ($numrows >= 0) {
+      $Dialect=$this->objDB->Dialect;
+    } else {
+      $numrows=$this->AllRowsMax;
+      $Dialect="";  // don't use limit query
+    }
+    switch ($this->objDB->Dialect) {
+      case "MySQL": $this->orderByRef=true; break;
+    }
+    $this->arParams=array('H'=>array(), 'W'=>array());
+    $this->oParse= new sqlParse();
+    if (is_array($sqlselect)) {
+      $this->oParse->LoadArray($sqlselect);
+    } else {
+      $this->oParse->ParseSelect($sqlselect);
+    }
+    $this->ApplyQStringParms($filters);
+    $this->allParams=array_merge($this->arParams['W'],$this->arParams['H']);
+    echo $this->RowsStart;
+    switch ($Dialect) {
+
+      case "TSQL":
+        $this->objDB->SingleRecordQuery("select @@VERSION", $version);
+        if (is_string($sqlselect) && strtoupper(substr($sqlselect,0,7))!="SELECT ") {
+          $this->allParams=array();
+          $totcnt=$this->Query2xmlRaw($sqlselect, $offset, $numrows);
+        }
+        else if (preg_match("/SQL Server 200(5|8)/i",$version[0])) {
+          $this->sqltext=$this->UnparseWithRowNumber($offset, $numrows + 1, true);
+          $totcnt=$this->Query2xmlRaw_Limit($this->sqltext, $offset, $numrows, 1);
+        }
+        else {
+          $this->sqltext=$this->oParse->UnparseSelectSkip($this->HiddenCols);
+          $totcnt=$this->Query2xmlRaw($this->sqltext, $offset, $numrows);
+        }
+        break;
+
+      case "Oracle":
+        $this->sqltext=$this->UnparseWithRowNumber($offset, $numrows + 1, false);
+        $totcnt=$this->Query2xmlRaw_Limit($this->sqltext, $offset, $numrows, 1);
+        break;
+
+      case "MySQL":
+        $this->sqltext=$this->oParse->UnparseSelectSkip($this->HiddenCols)." LIMIT ".$offset.",".($numrows + 1);
+        $totcnt=$this->Query2xmlRaw_Limit($this->sqltext, $offset, $numrows, 0);
+        break;
+
+      case "PostgreSQL":
+        $this->sqltext=$this->oParse->UnparseSelect()." LIMIT ".($numrows + 1) . " OFFSET ".$offset;
+        $totcnt=$this->Query2xmlRaw_Limit($this->sqltext, $offset, $numrows, 0);
+        break;
+
+      default:
+        $this->sqltext=$this->oParse->UnparseSelectSkip($this->HiddenCols);
+        $totcnt=$this->Query2xmlRaw($this->sqltext, $offset, $numrows);
+        break;
+    }
+    echo $this->RowsEnd;
+    if ($this->sendDebugMsgs) {
+      $this->AppendResponse("debug",$this->objDB->db->lastQuery);
+    }
+    if (!$this->eof && $gettotal) {
+      $totcnt=$this->getTotalRowCount();
+    }
+    if ($this->fmt=="xml" || $this->fmt=="json") {
+      if ($this->eof) $this->AppendResponse("rowcount",$totcnt);
+    }
+    $this->oParse=NULL;
+    return $totcnt;
+  }
+
+
+  function Query2xmlDistinct($sqlselect, $colnum, $numrows, $filters=array(), $sqldistinct=false) {
+    if ($numrows < 0) $numrows=$this->AllRowsMax;
+    $this->arParams=array('H'=>array(), 'W'=>array());
+    $this->oParse= new sqlParse();
+    if (is_array($sqlselect)) {
+      $this->oParse->LoadArray($sqlselect);
+    } else {
+      $this->oParse->ParseSelect($sqlselect);
+    }
+    $this->ApplyQStringParms($filters);
+    $this->allParams=array_merge($this->arParams['W'],$this->arParams['H']);
+    echo $this->RowsStart;
+    if ($sqldistinct) $this->sqltext=$sqldistinct; else
+    $this->sqltext=$this->oParse->UnparseDistinctColumn($colnum);
+    $totcnt=$this->Query2xmlRaw($this->sqltext, 0, $numrows);
+    echo $this->RowsEnd;
+    if ($this->sendDebugMsgs) {
+      $this->AppendResponse("debug",$this->objDB->db->lastQuery);
+    }
+    $this->oParse=NULL;
+  }
+
+
+  // Tested ok with SQL Server 2005, MySQL, and Oracle
+  function getTotalRowCount() {
+    $countSql="SELECT ".$this->oParse->UnparseColumnList()." FROM ".$this->oParse->FromClause;
+    if (!empty($this->oParse->WhereClause)) {
+      $countSql.=" WHERE ".$this->oParse->WhereClause;
+    }
+    if (is_array($this->oParse->arGroupBy)) {
+      if (count($this->oParse->arGroupBy) >  0) {
+        $countSql.=" GROUP BY ".implode(",",$this->oParse->arGroupBy);
+      }
+    }
+    if (!empty($this->oParse->HavingClause)) {
+      $countSql.=" HAVING ".$this->oParse->HavingClause;
+    }
+    $countSql="SELECT COUNT(*) FROM (".$countSql.")";
+    if ($this->objDB->Dialect != "Oracle") {
+      $countSql.=" AS rico_Main";
+    }
+    if (count($this->allParams)>0) {
+      $rsMain=$this->objDB->RunParamQuery($countSql,$this->allParams);
+    } else {
+      $rsMain=$this->objDB->RunQuery($countSql);
+    }
+    if (!$rsMain) {
+      echo "\n<debug>getTotalRowCount: rsMain is null</debug>";
+      return;
+    }
+    if (!$this->objDB->db->FetchArray($rsMain,$a)) return;
+    $this->objDB->rsClose($rsMain);
+    $this->eof=true;
+    return $a[0];
+  }
+
+
+  function UnparseWithRowNumber($offset, $numrows, $includeAS) {
+    if (is_array($this->oParse->arOrderBy)) {
+      if (count($this->oParse->arOrderBy) >  0) {
+        $strOrderBy=implode(",",$this->oParse->arOrderBy);
+      }
+    }
+    if (empty($strOrderBy) && !preg_match("/\bjoin\b/",$this->oParse->FromClause)) {
+      // order by clause should be included in main sql select statement
+      // However, if it isn't, then use primary key as sort - assuming FromClause is a simple table name
+      $strOrderBy=$this->objDB->PrimaryKey($this->oParse->FromClause);
+    }
+    $unparseText="SELECT ROW_NUMBER() OVER (ORDER BY ".$strOrderBy.") AS rico_rownum,";
+    $unparseText.=$this->oParse->UnparseColumnListSkip($this->HiddenCols)." FROM ".$this->oParse->FromClause;
+    if (!empty($this->oParse->WhereClause)) {
+      $unparseText.=" WHERE ".$this->oParse->WhereClause;
+    }
+    if (is_array($this->oParse->arGroupBy)) {
+      if (count($this->oParse->arGroupBy) >  0) {
+        $unparseText.=" GROUP BY ".implode(",",$this->oParse->arGroupBy);
+      }
+    }
+    if (!empty($this->oParse->HavingClause)) {
+      $unparseText.=" HAVING ".$this->oParse->HavingClause;
+    }
+    $unparseText="SELECT * FROM (".$unparseText.")";
+    if ($includeAS) {
+      $unparseText.=" AS rico_Main";
+    }
+    $unparseText.=" WHERE rico_rownum > ".$offset." AND rico_rownum <= ".($offset + $numrows);
+    return $unparseText;
+  }
+
+  function Query2xmlRaw($rawsqltext, $offset, $numrows) {
+    if (count($this->allParams)>0) {
+      $rsMain=$this->objDB->RunParamQuery($rawsqltext,$this->allParams);
+    } else {
+      $rsMain=$this->objDB->RunQuery($rawsqltext);
+    }
+    if (!$rsMain) return;
+  
+    $colcnt = $this->objDB->db->NumFields($rsMain);
+    $totcnt = $this->objDB->db->NumRows($rsMain);
+    //echo "<debug>Query2xmlRaw: NumRows=$totcnt</debug>";
+    if ($offset < $totcnt || $totcnt==-1) {
+      $this->objDB->db->Seek($rsMain,$offset);
+      switch ($this->fmt) {
+        case "json": $rowcnt=$this->WriteRowsJSON($rsMain, $numrows, 0); break;
+        default:     $rowcnt=$this->WriteRowsXHTML($rsMain, $numrows, 0); break;
+      }
+      if ($totcnt < 0) {
+        $totcnt=$offset+$rowcnt;
+        while($this->objDB->db->FetchRow($rsMain,$row))
+          $totcnt++;
+      }
+    } else {
+      $totcnt=$offset;
+    }
+    $this->objDB->rsClose($rsMain);
+    $this->eof=true;
+    return $totcnt;
+  }
+
+  function Query2xmlRaw_Limit($rawsqltext, $offset, $numrows, $firstcol) {
+    if (count($this->allParams)>0) {
+      $rsMain=$this->objDB->RunParamQuery($rawsqltext,$this->allParams);
+    } else {
+      $rsMain=$this->objDB->RunQuery($rawsqltext);
+    }
+    //if ($this->objDB->db->HasError()) echo "<error>" . $this->objDB->db->ErrorMsg() . "</error>";
+    $totcnt=$offset;
+    $this->eof=true;
+    if (!$rsMain) return;
+    switch ($this->fmt) {
+      case "json": $totcnt+=$this->WriteRowsJSON($rsMain, $numrows, $firstcol); break;
+      default:     $totcnt+=$this->WriteRowsXHTML($rsMain, $numrows, $firstcol); break;
+    }
+    $this->objDB->rsClose($rsMain);
+    return $totcnt;
+  }
+
+  function WriteRowsXHTML($rsMain, $numrows, $firstcol) {
+    $colcnt = $this->objDB->db->NumFields($rsMain);
+    $rowcnt=0;
+    if ($this->SendHdg) {
+      echo "\n<tr>";
+      for ($i=$firstcol; $i < $colcnt; $i++) {
+        $n=empty($this->Headings[$i-$firstcol]) ? $this->objDB->db->FieldName($rsMain,$i) : $this->Headings[$i-$firstcol];
+        print $this->XmlStringCell($n);
+      }
+      echo "</tr>";
+    }
+    while(($this->objDB->db->FetchRow($rsMain,$row)) && $rowcnt < $numrows) {
+      $rowcnt++;
+      print "\n<tr>";
+      for ($i=$firstcol; $i < $colcnt; $i++)
+        print $this->XmlStringCell($row[$i]);
+      print "</tr>";
+    }
+    $this->eof=($rowcnt < $numrows);
+    return $rowcnt;
+  }
+  
+  function WriteRowsJSON($rsMain, $numrows, $firstcol) {
+    $colcnt = $this->objDB->db->NumFields($rsMain);
+    $rowcnt=0;
+    if ($this->SendHdg) {
+      echo "\n[";
+      for ($i=$firstcol; $i < $colcnt; $i++) {
+        //$n=empty($this->Headings($i-$firstcol)) ? $this->objDB->db->FieldName($rsMain,$i) : $this->Headings($i-$firstcol);
+        print json_encode($n);
+      }
+      echo "]";
+    }
+    while(($this->objDB->db->FetchRow($rsMain,$row)) && $rowcnt < $numrows) {
+      if ($rowcnt>0 || $this->SendHdg) echo ",";
+      $rowcnt++;
+      print "\n[";
+      for ($i=$firstcol; $i < $colcnt; $i++) {
+        if ($i>$firstcol) echo ",";
+        print json_encode($this->convertCharSet ? utf8_encode($row[$i]) : $row[$i]);
+      }
+      print "]";
+    }
+    $this->eof=($rowcnt < $numrows);
+    return $rowcnt;
+  }
+  
+  function SetDbConn(&$dbcls) {
+    $this->objDB=&$dbcls;
+  }
+
+  function SetGridDefinition($def) {
+    $this->gridDefinition = $def;
+  }
+
+  function PushParam($newvalue) {
+    $parm=$this->convertCharSet ? utf8_decode($newvalue) : $newvalue;
+    if (get_magic_quotes_gpc()) $parm=stripslashes($parm);
+    array_push($this->arParams[$this->condType], $parm);
+    if ($this->sendDebugMsgs) {
+      echo "\n<debug>".$this->condType." param=".htmlspecialchars($parm)."</debug>";
+    }
+  }
+  
+  function setCondType($selectItem) {
+    $this->condType=(preg_match("/\bmin\(|\bmax\(|\bsum\(|\bcount\(/i",$selectItem) && !preg_match("/\bselect\b/i",$selectItem)) ? 'H' : 'W';
+  }
+  
+  function addCondition($newfilter) {
+    switch ($this->condType) {
+      case 'H': $this->oParse->AddHavingCondition($newfilter); break;
+      case 'W': $this->oParse->AddWhereCondition($newfilter); break;
+    }
+  }
+
+  function ApplyQStringParms($filters) {
+    foreach($_GET as $qs => $value) {
+      $prefix=substr($qs,0,1);
+      switch ($prefix) {
+
+        // user-invoked condition
+        case "w":
+        case "h":
+          $i=substr($qs,1);
+          if (!is_numeric($i)) break;
+          $i=intval($i);
+          if ($i<0 || $i>=count($filters)) break;
+          $newfilter=$filters[$i];
+          $this->condType=strtoupper($prefix);
+
+          $j=strpos($newfilter," in (?)");
+          if ($j !== false) {
+            $a=explode(",", $value);
+            for ($i=0; $i < count($a); $i++) {
+              $this->PushParam($a[$i]);
+              $a[$i]="?";
+            }
+            $newfilter=substr($newfilter,0,$j+4) . implode(",",$a) . substr($newfilter,$j+5);
+          } elseif (strpos($newfilter,"?") !== false) {
+            $this->PushParam($value);
+          }
+
+          $this->addCondition($newfilter);
+          break;
+
+        // sort
+        case "s":
+          $i=substr($qs,1);
+          if (!is_numeric($i)) break;
+          $i=intval($i);
+          if ($i<0 || $i>=count($this->oParse->arSelList)) break;
+          $value=strtoupper(substr($value,0,4));
+          if (!in_array($value,array('ASC','DESC'))) $value="ASC";
+          if ($this->orderByRef)
+            $this->oParse->AddSort(($i + 1)." ".$value);
+          else
+            $this->oParse->AddSort($this->oParse->arSelList[$i]." ".$value);
+          break;
+
+        // user-supplied filter
+        case "f":
+          //print_r($value);
+          foreach($value as $i => $filter) {
+            if ($i<0 || $i>=count($this->oParse->arSelList)) break;
+           switch ($this->objDB->Dialect) {
+           case "PostgreSQL":
+             if ($filter['op'] == 'LIKE' &&
+                 array_key_exists($this->oParse->arSelList[$i], $this->gridDefinition['list']) &&
+                 array_key_exists('sqltype', $this->gridDefinition['list'][$this->oParse->arSelList[$i]]) &&
+                 in_array($this->gridDefinition['list'][$this->oParse->arSelList[$i]]['sqltype'], array('date','int'))) {
+               $newfilter=sprintf('cast(%s as varchar(100))', $this->oParse->arSelList[$i]);
+             } else
+               $newfilter=$this->oParse->arSelList[$i];
+             break;
+           default:
+             $newfilter=$this->oParse->arSelList[$i];
+             break;
+           }
+
+            $this->setCondType($newfilter);
+            switch ($filter['op']) {
+              case "EQ":
+               if ($this->objDB->Dialect == 'PostgreSQL' &&
+                   array_key_exists($this->oParse->arSelList[$i], $this->gridDefinition['list']) &&
+                   array_key_exists('sqltype', $this->gridDefinition['list'][$this->oParse->arSelList[$i]]) &&
+                   in_array($this->gridDefinition['list'][$this->oParse->arSelList[$i]]['sqltype'], array('int')))
+                 $newfilter='('.$this->AddCoalesce($newfilter,'0').' IN '.$this->GetMultiParmFilter($filter).')';
+               else
+                $newfilter='('.$this->AddCoalesce($newfilter).' IN '.$this->GetMultiParmFilter($filter).')';
+                break;
+              case "LE":
+                $newfilter.="<=?";
+                $this->PushParam($filter[0]);
+                break;
+              case "GE":
+                $newfilter.=">=?";
+                $this->PushParam($filter[0]);
+                break;
+              case "NULL": $newfilter.=" is null"; break;
+              case "NOTNULL": $newfilter.=" is not null"; break;
+              case "LIKE":
+                $newfilter.=" LIKE ?";
+                $this->PushParam(str_replace("*",$this->objDB->Wildcard,$filter[0]));
+                break;
+              case "NE":
+               if ($this->objDB->Dialect == 'PostgreSQL' &&
+                   array_key_exists($this->oParse->arSelList[$i], $this->gridDefinition['list']) &&
+                   array_key_exists('sqltype', $this->gridDefinition['list'][$this->oParse->arSelList[$i]]) &&
+                   in_array($this->gridDefinition['list'][$this->oParse->arSelList[$i]]['sqltype'], array('int')))
+                 $newfilter='('.$this->AddCoalesce($newfilter,0).' NOT IN '.$this->GetMultiParmFilter($filter).')';
+               else
+                $newfilter='('.$this->AddCoalesce($newfilter).' NOT IN '.$this->GetMultiParmFilter($filter).')';
+                break;
+            }
+            $this->addCondition($newfilter);
+          }
+          break;
+      }
+    }
+  }
+
+  function AddCoalesce($newfilter, $default=false) {
+    if ($this->objDB->Dialect=="Access") {
+      return "iif(IsNull(" . $newfilter . "),''," . $newfilter . ")";
+    } else {
+      return "coalesce(" . $newfilter . "," . ($default !== false ? $default : "''") . ")";
+    }
+  }
+
+
+  function GetMultiParmFilter($filter) {
+    $flen=$filter['len'];
+    if (!is_numeric($flen)) return "";
+    $flen=intval($flen);
+    $newfilter='(';
+    for ($j=0; $j<$flen; $j++) {
+      if ($j > 0) $newfilter.=",";
+      $newfilter.='?';
+      $this->PushParam($filter[$j]);
+    }
+    $newfilter.=')';
+    return $newfilter;
+  }
+
+  function XmlStringCell($value) {
+    if (!isset($value)) {
+      $result="";
+    }
+    else {
+      if ($this->convertCharSet) {
+        $value=utf8_encode($value);
+        $result=htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
+      } else {
+        $result=htmlspecialchars($value);
+      }
+    }
+    if ($this->fmt=="html" && $result=="") $result="&nbsp;";
+    return "<td>".$result."</td>";
+  }
+
+  // for the root node, parentID should "" (empty string)
+  // containerORleaf: L/zero (leaf), C/non-zero (container)
+  // selectable:      0->not selectable, 1->selectable
+  function WriteTreeRow($parentID, $ID, $description, $containerORleaf, $selectable) {
+    echo "\n<tr>";
+    echo $this->XmlStringCell($parentID);
+    echo $this->XmlStringCell($ID);
+    echo $this->XmlStringCell($description);
+    echo $this->XmlStringCell($containerORleaf);
+    echo $this->XmlStringCell($selectable);
+    echo "</tr>";
+  }
+
+}
+
+?>
+
diff --git a/lib/rico3/ricoClient/css/rico.css b/lib/rico3/ricoClient/css/rico.css
new file mode 100644 (file)
index 0000000..ad9e1cd
--- /dev/null
@@ -0,0 +1,983 @@
+/* \r
+ *Rico stylesheet\r
+ */\r
+\r
+/* reset Themeroller font size */ \r
+div.ui-widget {\r
+  font-size: 80%;\r
+}\r
+.ui-dialog {\r
+  width: auto !important;\r
+}\r
+\r
+/* LiveGrid & SimpleGrid */\r
\r
+.ricoLG_outerDiv {\r
+  position:relative;\r
+}\r
+\r
+div.ricoLG_innerDiv, div.ricoLG_frozenTabsDiv {\r
+  overflow:hidden;\r
+  margin:0px;\r
+  padding:0px;\r
+  position:relative;\r
+}\r
+\r
+div.ricoLG_scrollDiv {\r
+  overflow:scroll;\r
+  position:relative;\r
+}\r
+\r
+div.ricoLG_scrollContainerDiv {\r
+  position:relative;\r
+}\r
+\r
+div.ricoLG_scrollTabDiv {\r
+  position:absolute;\r
+  top:0px;\r
+  overflow:hidden;\r
+}\r
+\r
+div.ricoLG_resizeDiv {\r
+  position:absolute;\r
+  top:0px;\r
+  width:1px;\r
+  z-index:2;\r
+  background-color:blue;\r
+}\r
+\r
+div.ricoLG_highlightDiv {\r
+  position:absolute;\r
+  border: 2px solid black;\r
+}\r
+\r
+.ricoLG_table, .ricoLG_scrollTab, table.ricoLiveGrid {\r
+  margin: 0px;\r
+  padding: 0px;\r
+  border-right: 1px solid silver;\r
+  border-left-width: 0px;  /* for dojo */\r
+}\r
+\r
+.ricoLG_FilterRow div.ricoLG_cell {\r
+  height:1.6em;\r
+  line-height: 1.6em;\r
+  white-space: nowrap;\r
+}\r
+.ricoLG_FilterRow  div.ricoLG_cell * {\r
+  vertical-align: top;\r
+}\r
+.ricoLG_FilterRow select {\r
+  width:100%; \r
+}\r
+\r
+.ricoLG_FilterRow input, .ricoLG_FilterRow select, .ricoLG_FilterRow option {\r
+  font-weight: normal;\r
+  font-size: 90% !important;\r
+  padding: 1px;\r
+}\r
+\r
+div.ricoLG_mFilter {\r
+  position:absolute;\r
+  z-index:200;\r
+  border: 1px solid #888;\r
+}\r
+div.ricoLG_mFilter {\r
+  font-size: 67%;\r
+}\r
+div.ricoLG_mFilter_content {\r
+  height: 150px;\r
+  width: 100%;\r
+  overflow: auto;\r
+  background-color: white;\r
+}\r
+div.ricoLG_mFilter_button  button{\r
+  font-size: 80%;\r
+}\r
+div.ricoLG_mFilter_button {\r
+  padding: 3px;\r
+  background-color: #DDD;\r
+}\r
+tr.ricoLG_mFilter_oddrow {\r
+  background-color: #EEE;\r
+}\r
+\r
+table.ricoLG_bottom {\r
+  border-top-style: none;\r
+}\r
+\r
+.ricoLG_selection { background-color: #cedebd; }\r
+\r
+div.ricoLG_col {\r
+  overflow:hidden;\r
+  width:100px;\r
+}\r
+\r
+thead.ricoLG_top div.ricoLG_col {\r
+  position:relative;\r
+}\r
+\r
+thead.ricoLG_top div.ricoLG_Resize {\r
+  position:absolute;\r
+  width:5px;\r
+  height:100%;\r
+  top:0px;\r
+  cursor:e-resize;\r
+}\r
+\r
+.ricoLG_bottom div.ricoLG_cell {\r
+  border-style: solid;\r
+  border-color: silver;\r
+  border-width: 0px 0px 1px 0px;\r
+}\r
+\r
+table.ricoLG_table, table.ricoLiveGrid  {\r
+  border-top: 1px solid silver;\r
+}\r
+\r
+thead.ricoLG_top th, thead.ricoLG_top td, table.ricoLiveGrid td, table.ricoLiveGrid th {\r
+  border-style: solid;\r
+  border-color: silver;\r
+  border-width: 0px 0px 1px 1px;\r
+  *position: relative; /* IE6-7  only */\r
+}\r
+\r
+.ricoLG_bottom th, .ricoLG_bottom td {\r
+  border-style: solid;\r
+  border-color: silver;\r
+  border-width: 0px 0px 0px 1px;\r
+}\r
+\r
+div.ricoLG_cell, table.ricoLiveGrid td, table.ricoLiveGrid th {\r
+  overflow:hidden;\r
+  height:1.2em;\r
+  padding-left: 3px;\r
+  margin: 0px;\r
+  font-size: 80%;\r
+  padding-top:3px;\r
+  padding-bottom:3px;\r
+}\r
+\r
+.ricoLG_messageDiv {\r
+  font-weight:bold;\r
+  font-size:larger;\r
+  text-align:center;\r
+  padding:1em;\r
+  background-color: #EEE;\r
+  opacity:0.8;\r
+}\r
+\r
+p.ricoBookmark {\r
+  margin-bottom: 3px;\r
+  font-size: 80%;\r
+  white-space: nowrap;\r
+}\r
+\r
+p.ricoBookmark span {\r
+  margin-right: 1em;\r
+}\r
+\r
+span.ricoCaption {\r
+  font-weight: bold;\r
+}\r
+\r
+span.ricoSessionTimer {\r
+  background-color:black;\r
+  color:white;\r
+}\r
+\r
+/* grid column chooser */\r
+\r
+div.ricoLG_chooserDiv {\r
+  z-index:200;\r
+}\r
+div.ricoLG_chooserDiv  .ricoContent {\r
+  height: 13em;\r
+  width: 16em;\r
+}\r
+.ricoContent {\r
+  padding: 3px;\r
+  overflow: auto;\r
+}\r
+div.ricoLG_chooserDiv .ricoContent div {\r
+  border-bottom: 1px solid #CCC;\r
+}\r
+div.ricoLG_chooserDiv .ricoContent input {\r
+  margin-right: 0.5em;\r
+}\r
+\r
+/* grid keyword entry */\r
+\r
+div.ricoLG_chooserDiv, div.ricoLG_keywordDiv  {\r
+  font-size: smaller;\r
+}\r
+div.ricoLG_keywordDiv {\r
+  position:absolute;\r
+  z-index:200;\r
+  width: 250px;\r
+}\r
+\r
+div.ricoLG_keywordDiv input {\r
+  position: relative;\r
+  font-size: 90%;\r
+  float: left;\r
+}\r
+\r
+\r
+/* LiveGrid Forms */\r
+\r
+span.ricoSaveMsg {\r
+  background-color:yellow;\r
+}\r
+\r
+span.ricoSessionTimer {\r
+  background-color:black;\r
+  color:white;\r
+}\r
+\r
+div.ricoLG_editResponseDiv {\r
+  color:#000; background:#E8ECF3;\r
+  overflow:auto;\r
+  padding:8px;\r
+  border: 1px solid navy;\r
+  position:absolute;\r
+  z-index:300;\r
+  top:0px;\r
+  left:0px;\r
+  font-size: 85%;\r
+}\r
+\r
+form .ricoEditLabel sup {\r
+  font-size: smaller;\r
+}\r
+\r
+form .ricoEditLabel {\r
+  font-weight: bold;\r
+  text-align: left;\r
+  padding-right: 1em;\r
+}\r
+\r
+.ricoLG_editDiv a.RicoButton {\r
+  text-decoration: none;\r
+  padding: 0.3em 1em;\r
+  margin-right: 6px;\r
+}\r
+\r
+.ricoLG_editDiv div.ButtonBar {\r
+  padding: 0.4em 0px;\r
+  margin: 3px;\r
+}\r
+\r
+.ricoLG_editDiv form {\r
+  margin:0px;\r
+}\r
+\r
+.ricoLG_editDiv textarea {\r
+  font-size: 100%;\r
+}\r
+\r
+.ricoLG_editDiv .tabContent, div.ricoLG_editDiv .noTabContent {\r
+  overflow: hidden;\r
+  padding: 4px;\r
+  white-space:nowrap;\r
+}\r
+\r
+\r
+/* shadows */\r
+.ricoShadow {\r
+  -moz-box-shadow: 3px 3px 4px #888;\r
+  -webkit-box-shadow: 3px 3px 4px #888;\r
+  box-shadow: 3px 3px 4px #888;\r
+  /* For IE 8 */\r
+  -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=5, Direction=135, Color='#aaaaaa')";\r
+  /* For IE 5.5 - 7 */\r
+  filter: progid:DXImageTransform.Microsoft.Shadow(Strength=5, Direction=135, Color='#aaaaaa');\r
+}\r
+\r
+\r
+/* ricoMenu */\r
+\r
+div.ricoMenu, div.ricoMenuSafari {\r
+border:1px solid #666;\r
+padding:2px;\r
+cursor:default;\r
+font-family:tahoma,arial,helvetica,sans-serif;\r
+font-size: 70%;\r
+}\r
+\r
+div.ricoMenu, div.ricoMenu a {\r
+background-color:menu;\r
+color: menutext;\r
+text-decoration: none;\r
+display:block;\r
+}\r
+\r
+div.ricoMenuSafari, div.ricoMenuSafari a {\r
+background-color:#EDEDED;\r
+text-decoration: none;\r
+display:block;\r
+}\r
+\r
+div.ricoMenu div.ricoMenuHeading{\r
+padding: 1px 0px;\r
+font-weight:bold;\r
+}\r
+\r
+div.ricoMenuSafari div.ricoMenuHeading{\r
+padding: 1px 0px;\r
+color: black;\r
+display: block;\r
+font-weight:bold;\r
+}\r
+\r
+div.ricoMenu .enabled {\r
+position: relative;\r
+}\r
+\r
+div.ricoMenuSafari .enabled {\r
+color: black;\r
+}\r
+\r
+div.ricoMenu .enabled, div.ricoMenu .enabled-hover, div.ricoMenuSafari .enabled, div.ricoMenuSafari .enabled-hover, div.ricoMenu .disabled, div.ricoMenuSafari .disabled {\r
+padding-left: 1em;\r
+padding-top:0.1em;\r
+padding-bottom:0.1em;\r
+z-index: 101;\r
+}\r
+\r
+div.ricoMenu .disabled, div.ricoMenuSafari .disabled {\r
+color: #999;\r
+}\r
+\r
+div.ricoMenu hr{\r
+height:1px;\r
+margin:1px;\r
+border:0;\r
+color: menu;\r
+background-color: menu;\r
+}\r
+\r
+div.ricoMenu .enabled-hover, div.ricoMenu .ricoSubMenuOpen {\r
+   background-color: Highlight;\r
+   color:            HighlightText;\r
+}\r
+\r
+div.ricoMenuSafari .enabled-hover, div.ricoMenuSafari .ricoSubMenuOpen {\r
+   background-color: #1657B8;\r
+   color:            white;\r
+}\r
+\r
+div.ricoMenu .ricoSubMenu, div.ricoMenu .ricoSubMenuOpen, div.ricoMenuSafari .ricoSubMenu, div.ricoMenuSafari .ricoSubMenuOpen {\r
+padding: 1px 0px;\r
+display: block;\r
+font-weight:bold;\r
+z-index: 101;\r
+position: relative;\r
+}\r
+\r
+div.ricoMenu div.ricoMenuBreak, div.ricoMenuSafari div.ricoMenuBreak {\r
+height:1px;\r
+margin:3px 0 3px 0;\r
+padding:0;\r
+background-color: #AAA;\r
+width:100%;\r
+line-height:5px;\r
+overflow:hidden;\r
+}\r
+\r
+a.RicoButton {\r
+  -moz-border-radius: 6px;\r
+  -webkit-border-radius: 6px;\r
+  border-radius: 6px;\r
+}\r
+\r
+\r
+/* Rico.Window */\r
+\r
+\r
+.ricoTitle {\r
+  padding: 3px;\r
+  cursor: move;\r
+}\r
+\r
+.ricoTitleSpan {\r
+  margin-right: 25px;\r
+  white-space: nowrap;\r
+}\r
+\r
+a.RicoButtonAnchorNative {\r
+  background-color: #DDD;\r
+  border: 1px outset;\r
+}\r
+a.RicoButtonAnchorNative  span {\r
+  vertical-align: middle;\r
+}\r
+a.RicoButtonAnchor {\r
+  padding: 1px;\r
+  margin: 0px 3px;\r
+  cursor: pointer;\r
+  font-size: 16px;\r
+}\r
+a.RicoButtonAnchor span {\r
+  display: inline-block;\r
+}\r
+\r
+a.RicoButtonAnchorNative:hover {\r
+  border:1px solid #666;\r
+  background-color: #CCC;\r
+}\r
+\r
+* html .ui-dialog-titlebar .ui-dialog-titlebar-close {\r
+  top: 16px;   /* required by IE6 */\r
+}\r
+\r
+.ricoLG_cell .ui-icon {\r
+  text-indent: 0px !important;\r
+}\r
+\r
+\r
+\r
+/* ricoKeywordSearch */\r
+.ricoKeywordSearch {\r
+  font-size: 70%;\r
+}\r
+\r
+\r
+/* ricoTree */\r
+\r
+.ricoTreeContainer {\r
+  background-color:#cedebd;\r
+  border:1px solid black;\r
+  padding:4px;\r
+}\r
+\r
+.ricoTree td {\r
+  padding: 0px;\r
+}\r
+\r
+.ricoTree {\r
+  border:thin inset;\r
+  overflow:auto;\r
+  background-color:#FFF;\r
+}\r
+\r
+.ricoTree p, .ricoTree a {\r
+  margin:0px;\r
+  padding-left:0.3em;\r
+  white-space:nowrap;\r
+}\r
+\r
+.ricoTree a {\r
+  cursor:pointer;\r
+  text-decoration:none;\r
+}\r
+\r
+.ricoTree img {\r
+  margin:0px;\r
+  padding:0px;\r
+  display:block;\r
+}\r
+\r
+.ricoTreeContainer {\r
+  font-size:70%;\r
+  white-space:nowrap;\r
+}\r
+\r
+.ricoTreeButtons {\r
+  height:1.1em;\r
+}\r
+\r
+.ricoTreeButtons span:hover {\r
+  background-color:#deeecd;\r
+}\r
+\r
+\r
+/* ricoCalendar */\r
+\r
+.ricoCalContainer {\r
+  font-size:85%;\r
+  background-color:#eee;\r
+}\r
+.ricoCalBody {\r
+  font-size:70%;\r
+}\r
+.ricoCalMonthPrompt {\r
+  font-size:95%;\r
+}\r
+.ricoCalYearPrompt p {\r
+  font-size:70%;\r
+}\r
+\r
+td.RicoCalHeading {\r
+  text-align: left;\r
+  padding: 0px !important;\r
+}\r
+* html td.RicoCalHeading {\r
+  position: static !important; /* override Themeroller */\r
+}\r
+* html td.RicoCalHeading table {\r
+  float: left;\r
+}\r
+* html td.RicoCalHeading .RicoCloseAnchor {\r
+  position: static;\r
+  float: right;\r
+}\r
+\r
+.ui-datepicker td.RicoCalHeading  table{\r
+  width: auto;\r
+  margin: 0px 2em !important;\r
+}\r
+.ricoCalContainer td.RicoCalHeading  table{\r
+  width: auto;\r
+  margin: 2px 2em 0px 2em;\r
+}\r
+td.RicoCalHeading td {\r
+  padding: 0.2em 0px;\r
+}\r
+td.RicoCalHeading {\r
+  font-size:90%;\r
+}\r
+td.RicoCalHeading table a {\r
+  text-decoration: none;\r
+  font-weight: bold !important;\r
+  margin: 0px !important;\r
+  text-align: center;\r
+  border: 0px none !important;\r
+  font-family: "Lucida Console", Courier, "Courier New", monospace;\r
+}\r
+\r
+.ui-datepicker-header  span {\r
+  text-indent: 0px !important;\r
+  padding: 0px !important;\r
+}\r
+\r
+.ricoCalContainer .ricoCalBody  td.ricoSelectedDay {\r
+  font-weight:bold;\r
+  background-color: #FFFF66 !important;\r
+}\r
+\r
+.ricoCalContainer tbody td.hover {\r
+  background-color: #FDD;\r
+}\r
+\r
+span.Rico_leftArrow {\r
+  background-position: -10px -1046px;\r
+  display: inline-block;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+\r
+.ui-datepicker a.Rico_leftArrow, .ui-datepicker a.Rico_rightArrow {\r
+  display: inline-block;\r
+  width: 16px;\r
+  height: 16px;\r
+}\r
+\r
+.ricoCalContainer a.Rico_leftArrow, .ricoCalContainer a.Rico_rightArrow {\r
+  display: inline-block;\r
+  width: 16px;\r
+  height: 14px;\r
+}\r
+\r
+span.Rico_rightArrow {\r
+  background-position: -10px -1108px;\r
+  display: inline-block;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+\r
+.ui-datepicker-header a.ui-dialog-titlebar-close {\r
+  top: 2px;\r
+}\r
+\r
+div.ricoCalYearPrompt {\r
+  margin: 0px;\r
+  padding: 3px;\r
+  border:1px solid #666666;\r
+  background-color: #FEE;\r
+}\r
+\r
+.ricoCalYearPrompt p {\r
+  margin: 0px;\r
+  padding: 3px;\r
+}\r
+\r
+.ricoCalYearPrompt img {\r
+  border: 1px solid black;\r
+  margin-left: 3px;\r
+  vertical-align: middle;\r
+}\r
+\r
+.ricoCalFoot td {\r
+  font-size:75% !important;\r
+  text-align:center;\r
+  padding: 2px;\r
+  text-decoration: underline;\r
+  cursor:pointer;\r
+}\r
+\r
+td.ricoCalFoot {\r
+  color:#FFF;\r
+  background-color: #666666;\r
+}\r
+\r
+tr.ricoCalDayNames td {\r
+  font-weight: bold;\r
+/*  padding: 0px 2px 0px 2px;*/\r
+  padding: 0px !important;\r
+  text-align:right;\r
+}\r
+\r
+.ricoCalBody td {\r
+  width:2.7em;\r
+}\r
+\r
+td.ricoCal0, td.ricoCal1, td.ricoCal2, td.ricoCal3, td.ricoCal4, td.ricoCal5, td.ricoCal6, td.ricoCalToday {\r
+  text-align:right;\r
+  text-decoration:none;\r
+}\r
+\r
+td.ricoCal0, td.ricoCal1, td.ricoCal2, td.ricoCal3, td.ricoCal4, td.ricoCal5, td.ricoCal6 {\r
+  cursor:pointer;\r
+/*  padding-right: 2px !important;*/\r
+  padding: 0px;\r
+}\r
+\r
+/* Monday-Friday */\r
+.ricoCalContainer td.ricoCal1, .ricoCalContainer td.ricoCal2, .ricoCalContainer td.ricoCal3, .ricoCalContainer td.ricoCal4, .ricoCalContainer td.ricoCal5 {\r
+  color:black;\r
+  background-color:#fff;\r
+}\r
+\r
+/* Sunday, Saturday */\r
+.ricoCalContainer td.ricoCal0, .ricoCalContainer td.ricoCal6 {\r
+  color:#999;\r
+  background-color:#fff;\r
+}\r
+\r
+td.ricoCalToday {\r
+  cursor:pointer;\r
+  color:red;\r
+  font-weight:bold;\r
+}\r
+\r
+.ricoCalContainer  .ricoCalBody  td.ricoCalToday {\r
+  background-color: #33FFFF;\r
+}\r
+\r
+td.ricoCalWeekNum {\r
+  background-color: #D4D0C8;\r
+  color:black;\r
+  text-align:center;\r
+}\r
+\r
+.ricoCalMenu {\r
+  width:12em;\r
+  background-color: #FEE;\r
+  border-bottom:1px solid #666666;\r
+  border-right:1px solid #666666;\r
+}\r
+\r
+.ricoCalMenu td {\r
+  border-top:1px solid #666666;\r
+  border-left:1px solid #666666;\r
+}\r
+\r
+.ricoCalMenu a {\r
+  display:block;\r
+  text-decoration:none;\r
+  cursor:pointer;\r
+  font-size:70%;\r
+  text-align: center !important;\r
+  color:black;\r
+}\r
+\r
+.ricoCalMenu a:hover {\r
+  background-color: #FCC;\r
+}\r
+\r
+\r
+/* ricoColorPicker */\r
+\r
+div.ricoColorPicker {\r
+  background-color: white;\r
+}\r
+\r
+div.ricoColorPicker td {\r
+  width: 12px;\r
+  height: 12px;\r
+  font-size: 60%;\r
+}\r
+\r
+/* tabbed panels and accordion */\r
+\r
+.Rico_tabNavContainer {\r
+  display:block;\r
+  margin:0px;\r
+  padding: 0px;\r
+}\r
+\r
+.Rico_tabTitle {\r
+  float: left;\r
+  margin-left: 1px;\r
+  margin-right: 1px;\r
+  margin-bottom: 0px;\r
+  display: inline;\r
+}\r
+.Rico_tabContent, .Rico_accContent {\r
+  padding: 1em;\r
+}\r
+.Rico_accContent {\r
+  overflow: auto;\r
+}\r
+.Rico_tabContentContainer {\r
+  clear:both;\r
+}\r
+.Rico_tabNavContainer .hover, .Rico_accTitle {\r
+  cursor: pointer;\r
+}\r
+\r
+div.ricoCorner {\r
+  position: absolute;\r
+}\r
+div.ricoCorner div {\r
+  line-height: 1px;\r
+  font-size:1px;\r
+  overflow: hidden;\r
+}\r
+\r
+* html .Rico_tabPanel {\r
+  height: 1%;\r
+  position: relative;\r
+}\r
+\r
+* html .ui-tabs {\r
+  position: relative;\r
+}\r
+\r
+* html .Rico_tabNavContainer {\r
+  height: 1%;\r
+  position: relative;\r
+}\r
+\r
+.rico-calarrow {\r
+  width: 37px;\r
+  height: 18px;\r
+  display: inline-block;\r
+  background-position: -10px -10px;\r
+}\r
+.rico-calendaricon {\r
+  width: 17px;\r
+  height: 17px;\r
+  display: inline-block;\r
+  background-position: -10px -48px;\r
+}\r
+.rico-close-b {\r
+  background-position: -10px -122px;\r
+}\r
+.rico-close-w {\r
+  background-position: -10px -155px;\r
+}\r
+.rico-delete-b {\r
+  background-position: -10px -188px;\r
+}\r
+.rico-delete-w {\r
+  background-position: -10px -225px;\r
+}\r
+.rico-doc {\r
+  background-position: -10px -262px;\r
+}\r
+.rico-dotbutton {\r
+  width: 17px;\r
+  height: 17px;\r
+  display: inline-block;\r
+  background-position: -10px -304px;\r
+}\r
+.rico-drop {\r
+  background-position: -10px -341px;\r
+}\r
+.rico-folderclosed {\r
+  width: 24px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -403px;\r
+}\r
+.rico-folderopen {\r
+  width: 24px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -445px;\r
+}\r
+.rico-info {\r
+  width: 15px;\r
+  height: 15px;\r
+  display: inline-block;\r
+  background-position: -10px -487px;\r
+}\r
+.rico-link {\r
+  background-position: -10px -522px;\r
+}\r
+.rico-tree-m {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -564px;\r
+}\r
+.rico-tree-node {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -606px;\r
+}\r
+.rico-tree-nodelast {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -648px;\r
+}\r
+.rico-tree-nodeline {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -690px;\r
+}\r
+.rico-tree-nodem {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -732px;\r
+}\r
+.rico-tree-nodemlast {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -774px;\r
+}\r
+.rico-tree-nodep {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -816px;\r
+}\r
+.rico-tree-nodeplast {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -858px;\r
+}\r
+.rico-tree-p {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background-position: -10px -900px;\r
+}\r
+.rico-tree-nodeblank {\r
+  width: 16px;\r
+  height: 22px;\r
+  display: block;\r
+  background: none !important;\r
+}\r
+.rico-removeFilter {\r
+  background-position: -10px -942px;\r
+}\r
+.rico-FilterCollapse {\r
+  background-position: -10px -974px;\r
+}\r
+.rico-FilterExpand {\r
+  background-position: -10px -1010px;\r
+}\r
+.rico-left-b {\r
+  background-position: -10px -1046px;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+.rico-left-w {\r
+  background-position: -10px -1077px;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+.rico-right-b {\r
+  background-position: -10px -1108px;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+.rico-right-w {\r
+  background-position: -10px -1139px;\r
+  width: 10px;\r
+  height: 11px;\r
+}\r
+\r
+.ricoLG_filterCol {\r
+  margin: 0px 2px;\r
+  width: 13px;\r
+  height: 12px;\r
+  background-position: -10px -371px;\r
+}\r
+.ricoLG_sortAsc {\r
+  margin: 0px 2px;\r
+  width: 11px;\r
+  height: 11px;\r
+  background-position: -10px -1168px;\r
+}\r
+.ricoLG_sortDesc {\r
+  margin: 0px 2px;\r
+  width: 11px;\r
+  height: 11px;\r
+  background-position: -10px -1198px;\r
+}\r
+\r
+.ricoClearNative {\r
+  width: 17px;\r
+  height: 17px;\r
+  background-position: -10px -186px;\r
+}\r
+\r
+a span {\r
+  cursor: pointer;\r
+}\r
+\r
+span.ricoClear {\r
+  display:-moz-inline-box;\r
+  display:inline-block;\r
+  cursor: pointer;\r
+}\r
+\r
+span.RicoCheckmark {\r
+  display: inline-block;\r
+  height: 17px;\r
+  width: 17px;\r
+  background-position: -10px -85px;\r
+}\r
+\r
+span.RicoCancel {\r
+  display: inline-block;\r
+  height: 17px;\r
+  width: 17px;\r
+  background-position: -10px -188px;\r
+}\r
+\r
+\r
+.RicoCloseAnchor {\r
+  cursor: pointer;\r
+  position: absolute;\r
+  display: block;\r
+  top: 5px;\r
+  right: 2px;\r
+  border: 0px none !important;\r
+}\r
+span.RicoClose {\r
+  display: block;\r
+  height: 15px;\r
+  width: 16px;\r
+  background-position: -10px -122px;\r
+}\r
+\r
+/* ie6 only */\r
+* html iframe.RicoShim {\r
+  position: absolute;\r
+  display:block;\r
+  top: 0px;\r
+  left: 0px;\r
+  width: expression( this.previousSibling.offsetWidth+'px' );\r
+  height: expression( this.previousSibling.offsetHeight+'px' );\r
+  z-index: 1;\r
+}\r
diff --git a/lib/rico3/ricoClient/css/rico_icon.css b/lib/rico3/ricoClient/css/rico_icon.css
new file mode 100644 (file)
index 0000000..0893499
--- /dev/null
@@ -0,0 +1,8 @@
+.ricoLG_Resize {
+  background-repeat: repeat;
+  background-image: url('../../ricoClient/images/resize.gif');
+}
+.rico-icon {
+  background-repeat: no-repeat;
+  background-image: url('../../ricoClient/images/ricoIcons.gif');
+}
diff --git a/lib/rico3/ricoClient/css/striping_cupertino.css b/lib/rico3/ricoClient/css/striping_cupertino.css
new file mode 100644 (file)
index 0000000..c9c9c0f
--- /dev/null
@@ -0,0 +1,7 @@
+div.ricoLG_evenRow {  background-color: #ffffff; }\r
+div.ricoLG_oddRow {  background-color: #F9FAFB; }\r
+.ricoLG_bottom div.ui-state-hover {\r
+  border-left: none;\r
+  border-top: none;\r
+  border-right: none;\r
+}\r
diff --git a/lib/rico3/ricoClient/images/resize.gif b/lib/rico3/ricoClient/images/resize.gif
new file mode 100644 (file)
index 0000000..8efd1b5
Binary files /dev/null and b/lib/rico3/ricoClient/images/resize.gif differ
diff --git a/lib/rico3/ricoClient/images/ricoIcons.gif b/lib/rico3/ricoClient/images/ricoIcons.gif
new file mode 100644 (file)
index 0000000..cc67808
Binary files /dev/null and b/lib/rico3/ricoClient/images/ricoIcons.gif differ
diff --git a/lib/rico3/ricoClient/js/rico2jqu.js b/lib/rico3/ricoClient/js/rico2jqu.js
new file mode 100644 (file)
index 0000000..8d0447a
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+  *  Copyright (c) 2009-2011 Matt Brown
+  *
+  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+  *  file except in compliance with the License. You may obtain a copy of the License at
+  *
+  *         http://www.apache.org/licenses/LICENSE-2.0
+  *
+  *  Unless required by applicable law or agreed to in writing, software distributed under the
+  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+  *  either express or implied. See the License for the specific language governing permissions
+  *  and limitations under the License.
+  **/
+
+if (typeof jQuery=='undefined') throw('This version of Rico requires the jQuery library');
+
+var Rico = {
+  Lib: 'jQuery',
+  LibVersion: jQuery().jquery,
+  extend: jQuery.extend,
+  trim: jQuery.trim,
+  tryFunctions: function() {
+    for (var i=0; i<arguments.length; i++) {
+      try {
+        return arguments[i]();
+      } catch(e){}
+    }
+    return null;
+  },
+
+  _j: function(element) {
+    if (typeof element=='string')
+      element = document.getElementById(element);
+    return jQuery(element);
+  },
+
+  select: function(selector, element) {
+    return element ? this._j(element).find(selector) : jQuery(selector);
+  },
+
+  eventBind: function(element, eventName, handler) {
+    this._j(element).bind(eventName, handler);
+  },
+
+  eventUnbind: function(element, eventName, handler) {
+    this._j(element).unbind(eventName, handler);
+  },
+
+  eventHandle: function(object, method) {
+    return function(e) {
+      return object[method].call(object,e);
+    }
+  },
+
+  eventElement: function(ev) {
+    return ev.target;
+  },
+
+  eventClient: function(ev) {
+    return {x:ev.clientX, y:ev.clientY};
+  },
+
+  eventStop: function(ev) {
+    ev.preventDefault();
+    ev.stopPropagation();
+  },
+
+  addClass: function(element, className) {
+    var j=this._j(element);
+    if (!j.hasClass(className)) j.addClass(className);
+    return j;
+  },
+
+  removeClass: function(element, className) {
+    return this._j(element).removeClass(className);
+  },
+
+  hasClass: function(element, className) {
+    return this._j(element).hasClass(className);
+  },
+
+  getStyle: function(element, property) {
+    return this._j(element).css(property);
+  },
+  setStyle: function(element, properties) {
+    return this._j(element).css(properties);
+  },
+
+  /**
+   * @returns available height, excluding scrollbar & margin
+   */
+  windowHeight: function() {
+    return jQuery(window).height();
+  },
+
+  /**
+   * @returns available width, excluding scrollbar & margin
+   */
+  windowWidth: function() {
+    return jQuery(window).width();
+  },
+
+  positionedOffset: function(element) {
+    return this._j(element).position();
+  },
+
+  cumulativeOffset: function(element) {
+    return this._j(element).offset();
+  },
+
+  docScrollLeft: function() {
+    return jQuery('html').scrollLeft();
+  },
+
+  docScrollTop: function() {
+    return jQuery('html').scrollTop();
+  },
+
+  getDirectChildrenByTag: function(element, tagName) {
+    return this._j(element).children(tagName);
+  },
+
+  toQueryString: jQuery.param,
+
+  // Animation
+
+  fadeIn: function(element,duration,onEnd) {
+    this._j(element).fadeIn(duration,onEnd);
+  },
+
+  fadeOut: function(element,duration,onEnd) {
+    this._j(element).fadeOut(duration,onEnd);
+  },
+
+  animate: function(element,options,properties) {
+    options.complete=options.onEnd;
+    this._j(element).animate(properties,options);
+  },
+
+  getJSON: jQuery.httpData ? function(xhr) { return jQuery.httpData(xhr,'json'); } : function(xhr) { return jQuery.parseJSON(xhr.responseText); },
+
+  ajaxRequest: function(url,options) {
+    this.jSend(url,options);
+  }
+};
+
+Rico.ajaxRequest.prototype = {
+  jSend: function(url,options) {
+    this.onSuccess=options.onSuccess;
+    var self=this;
+    var jOptions = {
+      complete : options.onComplete,
+      error: options.onFailure,
+      success: function() { self.jSuccess(); },
+      type : options.method.toUpperCase(),
+      url : url,
+      data : options.parameters
+    }
+    this.xhr=jQuery.ajax(jOptions);
+  },
+
+  jSuccess: function() {
+    if (this.onSuccess) this.onSuccess(this.xhr);
+  }
+};
+
+Rico.ajaxSubmit=function(form,url,options) {
+  options.parameters=this._j(form).serialize();
+  if (!options.method) options.method='post';
+  url=url || form.action;
+  new Rico.ajaxRequest(url,options);
+};
diff --git a/lib/rico3/ricoClient/js/ricoThemeroller.js b/lib/rico3/ricoClient/js/ricoThemeroller.js
new file mode 100644 (file)
index 0000000..308daa2
--- /dev/null
@@ -0,0 +1,61 @@
+/*\r
+ *  (c) 2005-2009 Richard Cowin (http://openrico.org)\r
+ *  (c) 2005-2009 Matt Brown (http://dowdybrown.com)\r
+ *\r
+ *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this\r
+ *  file except in compliance with the License. You may obtain a copy of the License at\r
+ *\r
+ *         http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ *  Unless required by applicable law or agreed to in writing, software distributed under the\r
+ *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\r
+ *  either express or implied. See the License for the specific language governing permissions\r
+ *  and limitations under the License.\r
+ */\r
\r
+// Connects Rico to jQuery Themeroller css classes\r
+\r
+ Rico.theme = {\r
+  gridheader: 'ui-widget-header',\r
+  gridcontent:'ui-widget-content',\r
+  gridMessage:'ui-state-highlight ui-corner-all',\r
+  sortAsc:'ui-icon ui-icon-triangle-1-n',\r
+  sortDesc:'ui-icon ui-icon-triangle-1-s',\r
+  hover:'ui-state-hover',\r
+  selected:'ui-state-active',\r
+  button:'ui-state-default ui-corner-all',\r
+  gridHighlightClass:'ui-state-hover',\r
+  accordion:'ui-accordion ui-widget ui-helper-reset',\r
+  accTitle:'ui-accordion-header ui-helper-reset ui-state-default',\r
+  accContent:'ui-accordion-content ui-helper-reset ui-widget-content',\r
+  tabPanel:'ui-tabs ui-widget ui-widget-content ui-helper-clearfix',\r
+  tabNavContainer:'ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header',\r
+  tabTitle:'ui-state-default',\r
+  tabContent:'ui-tabs-panel ui-widget-content ui-corner-bottom',\r
+  tabCornerOptions: {corners:'top',nativeCorners:true},\r
+  tabSelected:'ui-tabs-selected ui-state-active',\r
+  calendar:'ui-datepicker ui-widget-content',\r
+  calendarHeading:'ui-datepicker-header ui-widget-header',\r
+  calendarSubheading:'ui-datepicker-title',\r
+  calendarTable:'ui-datepicker-calendar',\r
+  calendarDay:'ui-state-default',\r
+  calendarFooter:'ui-widget-footer',\r
+  calendarToday:'ui-state-highlight',\r
+  calendarSelectedDay:'ui-state-active',\r
+  calendarPopdown:'ui-widget-content',\r
+  tree:'ui-widget ui-widget-content',\r
+  treeContent:'ui-widget-content',\r
+  leftArrow:'ui-icon ui-icon-circle-triangle-w',\r
+  rightArrow:'ui-icon ui-icon-circle-triangle-e',\r
+  leftArrowAnchor:'ui-datepicker-prev',\r
+  rightArrowAnchor:'ui-datepicker-next',\r
+  dialog:'ui-dialog ui-widget ui-widget-content ui-draggable',\r
+  dialogTitle:'ui-dialog-titlebar ui-widget-header',\r
+  dialogContent:'ui-widget-content',\r
+  buttonAnchor:'ui-state-default',\r
+  checkmark:'ui-icon ui-icon-check',\r
+  closeAnchor:'ui-dialog-titlebar-close',\r
+  cancel:'ui-icon ui-icon-close',\r
+  clear:'ui-icon ui-icon-close',\r
+  close:'ui-icon ui-icon-closethick'\r
+}\r
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png b/lib/rico3/ui-cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png
new file mode 100644 (file)
index 0000000..6348115
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png b/lib/rico3/ui-cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png
new file mode 100644 (file)
index 0000000..7680b54
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png b/lib/rico3/ui-cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png
new file mode 100644 (file)
index 0000000..705a32e
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_glass_50_3baae3_1x400.png b/lib/rico3/ui-cupertino/images/ui-bg_glass_50_3baae3_1x400.png
new file mode 100644 (file)
index 0000000..baabca6
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_glass_50_3baae3_1x400.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png b/lib/rico3/ui-cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png
new file mode 100644 (file)
index 0000000..d9387e9
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png b/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png
new file mode 100644 (file)
index 0000000..28b566c
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png b/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png
new file mode 100644 (file)
index 0000000..d588297
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png b/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png
new file mode 100644 (file)
index 0000000..2289d3c
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png b/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png
new file mode 100644 (file)
index 0000000..0de3275
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-icons_2694e8_256x240.png b/lib/rico3/ui-cupertino/images/ui-icons_2694e8_256x240.png
new file mode 100644 (file)
index 0000000..e62b8f7
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-icons_2694e8_256x240.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-icons_2e83ff_256x240.png b/lib/rico3/ui-cupertino/images/ui-icons_2e83ff_256x240.png
new file mode 100644 (file)
index 0000000..09d1cdc
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-icons_2e83ff_256x240.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-icons_3d80b3_256x240.png b/lib/rico3/ui-cupertino/images/ui-icons_3d80b3_256x240.png
new file mode 100644 (file)
index 0000000..52c3cc6
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-icons_3d80b3_256x240.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-icons_72a7cf_256x240.png b/lib/rico3/ui-cupertino/images/ui-icons_72a7cf_256x240.png
new file mode 100644 (file)
index 0000000..0d20b73
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-icons_72a7cf_256x240.png differ
diff --git a/lib/rico3/ui-cupertino/images/ui-icons_ffffff_256x240.png b/lib/rico3/ui-cupertino/images/ui-icons_ffffff_256x240.png
new file mode 100644 (file)
index 0000000..42f8f99
Binary files /dev/null and b/lib/rico3/ui-cupertino/images/ui-icons_ffffff_256x240.png differ
diff --git a/lib/rico3/ui-cupertino/jquery-ui.css b/lib/rico3/ui-cupertino/jquery-ui.css
new file mode 100644 (file)
index 0000000..c1b423f
--- /dev/null
@@ -0,0 +1,563 @@
+/*!
+ * jQuery UI CSS Framework 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+/*!
+ * jQuery UI CSS Framework 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Lucida%20Grande,%20Lucida%20Sans,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=deedf7&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=100&borderColorHeader=aed0ea&fcHeader=222222&iconColorHeader=72a7cf&bgColorContent=f2f5f7&bgTextureContent=04_highlight_hard.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=362b36&iconColorContent=72a7cf&bgColorDefault=d7ebf9&bgTextureDefault=02_glass.png&bgImgOpacityDefault=80&borderColorDefault=aed0ea&fcDefault=2779aa&iconColorDefault=3d80b3&bgColorHover=e4f1fb&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=74b2e2&fcHover=0070a3&iconColorHover=2694e8&bgColorActive=3baae3&bgTextureActive=02_glass.png&bgImgOpacityActive=50&borderColorActive=2694e8&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=ffef8f&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=25&borderColorHighlight=f9dd34&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=cd0a0a&bgTextureError=01_flat.png&bgImgOpacityError=15&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=eeeeee&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=90&opacityOverlay=80&bgColorShadow=000000&bgTextureShadow=04_highlight_hard.png&bgImgOpacityShadow=70&opacityShadow=30&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; font-size: 1.1em; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #dddddd; background: #f2f5f7 url(images/ui-bg_highlight-hard_100_f2f5f7_1x100.png) 50% top repeat-x; color: #362b36; }
+.ui-widget-content a { color: #362b36; }
+.ui-widget-header { border: 1px solid #aed0ea; background: #deedf7 url(images/ui-bg_highlight-soft_100_deedf7_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
+.ui-widget-header a { color: #222222; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aed0ea; background: #d7ebf9 url(images/ui-bg_glass_80_d7ebf9_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #2779aa; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #2779aa; text-decoration: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #74b2e2; background: #e4f1fb url(images/ui-bg_glass_100_e4f1fb_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #0070a3; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #0070a3; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #2694e8; background: #3baae3 url(images/ui-bg_glass_50_3baae3_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ffffff; text-decoration: none; }
+.ui-widget :active { outline: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #f9dd34; background: #ffef8f url(images/ui-bg_highlight-soft_25_ffef8f_1x100.png) 50% top repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #cd0a0a url(images/ui-bg_flat_15_cd0a0a_40x100.png) 50% 50% repeat-x; color: #ffffff; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_72a7cf_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_72a7cf_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_72a7cf_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_3d80b3_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_2694e8_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; -khtml-border-top-left-radius: 6px; border-top-left-radius: 6px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; -khtml-border-top-right-radius: 6px; border-top-right-radius: 6px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; -khtml-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; -khtml-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #eeeeee url(images/ui-bg_diagonals-thick_90_eeeeee_40x40.png) 50% 50% repeat; opacity: .80;filter:Alpha(Opacity=80); }
+.ui-widget-shadow { margin: -7px 0 0 -7px; padding: 7px; background: #000000 url(images/ui-bg_highlight-hard_70_000000_1x100.png) 50% top repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*!
+ * jQuery UI Resizable 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*!
+ * jQuery UI Selectable 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
+/*!
+ * jQuery UI Accordion 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+/* IE/Win - Fix animation bug - #4615 */
+.ui-accordion { width: 100%; }
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
+.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
+.ui-accordion .ui-accordion-content-active { display: block; }
+/*!
+ * jQuery UI Autocomplete 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete { position: absolute; cursor: default; }      
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
+
+/*
+ * jQuery UI Menu 1.8.24
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu {
+       list-style:none;
+       padding: 2px;
+       margin: 0;
+       display:block;
+       float: left;
+}
+.ui-menu .ui-menu {
+       margin-top: -3px;
+}
+.ui-menu .ui-menu-item {
+       margin:0;
+       padding: 0;
+       zoom: 1;
+       float: left;
+       clear: left;
+       width: 100%;
+}
+.ui-menu .ui-menu-item a {
+       text-decoration:none;
+       display:block;
+       padding:.2em .4em;
+       line-height:1.5;
+       zoom:1;
+}
+.ui-menu .ui-menu-item a.ui-state-hover,
+.ui-menu .ui-menu-item a.ui-state-active {
+       font-weight: normal;
+       margin: -1px;
+}
+/*!
+ * jQuery UI Button 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; } 
+button.ui-button-icons-only { width: 3.7em; } 
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
+.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+/*!
+ * jQuery UI Dialog 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/*!
+ * jQuery UI Slider 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/*!
+ * jQuery UI Tabs 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
+/*!
+ * jQuery UI Datepicker 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}/*!
+ * jQuery UI Progressbar 1.8.24
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
\ No newline at end of file
diff --git a/lib/rico3/ui-cupertino/jquery-ui_hallinta.css b/lib/rico3/ui-cupertino/jquery-ui_hallinta.css
new file mode 100644 (file)
index 0000000..43a2016
--- /dev/null
@@ -0,0 +1,17 @@
+.ui-widget-content { background: none; }
+span.ricoTitleSpan { font-size: 120%; }
+div.ricoShadow.ricoWindow { border: 1px solid #555; padding: 0; }
+.ricoLG_col .ui-state-hover { font-weight: normal; }
+.ricoLG_cell a { color: #0066b3; }
+div.ricoLG_cell, table.ricoLiveGrid td, table.ricoLiveGrid th { font-size: 100%; height: 1.1em; }
+.ricoLG_FilterRow div.ricoLG_cell { height: 1.2em; }
+div.ricoMenu, div.ricoMenuSafari { font-size: 90%; }
+.alignleft { text-align: left; }
+.aligncenter { text-align: center; }
+.alignright { text-align: right; }
+div.ui-datepicker { background: #EEE; }
+.ui-datepicker table { font-size: inherit; }
+.ricoCalFoot td { font-size: 80%; }
+.ricoCalBody { font-size: 80%; }
+.ui-widget-content { border: 1px solid #dddddd; color: #362b36; }
+div.RicoWindow div.info { font-size: 80%; }
index e042e4b..17ff612 100644 (file)
@@ -10,25 +10,36 @@ if(typeof Rico=='undefined') throw("RicoExtension requires the Rico JavaScript f
 if(typeof Rico.TableColumn=='undefined') throw("RicoExtension requires ricoGridCommon.js");
 if(typeof Rico.TableColumn.checkbox=='undefined') throw("RicoExtension requires ricoGridControls.js");
 
-// Checkbox with database connection
-Rico.TableColumn.checkboxDB = Class.create(Rico.TableColumn.checkbox, {
+Rico.TableColumn.checkboxDB = function(refcol, update) {
+    this.initialize(refcol, update);
+};
+
+Rico.TableColumn.checkboxDB.prototype = Rico.extend(new Rico.TableColumn.checkbox(), {
+       initialize: function(refcol, update) {
+           this._checkedValue=1;
+           this._uncheckedValue=0
+           this._defaultValue=0
+           this._readOnly=0
+           this._checkboxes=[];
 
-       initialize: function($super, refcol, update) {
-           $super(1, 0, 0, 0);
            this._referenceColumn = refcol;
            this._updateUrl = update;
        },
 
-       _onclick: function($super,e) {
-           $super(e);
-           var elem=Event.element(e);
-           var windowRow=parseInt(elem.id.split(/_/).pop());
+       _onclick: function(e) {
+           var elem=Rico.eventElement(e);
+           var windowRow=parseInt(elem.id.substr((elem.id.lastIndexOf('_',elem.id.length)+1)));  //faster than split
+           var newval=elem.checked ? this._checkedValue : this._uncheckedValue;
+           this.setValue(windowRow,newval);
 
            this._update(windowRow);
        },
 
        _formbase: function() {
-           return 'table='+this.liveGrid.tableId+'&column='+this.index;
+           var formdata = 'table='+this.liveGrid.tableId+'&column='+this.index;
+           if (this.liveGrid.tableId.indexOf('__second') > -1)
+               formdata += '&main_id='+Hallinta.mainId;
+           return formdata;
        },
 
        _update: function(row) {
@@ -43,7 +54,7 @@ Rico.TableColumn.checkboxDB = Class.create(Rico.TableColumn.checkbox, {
             var id = this.liveGrid.columns[this._referenceColumn].getValue(row);
            var formdata = this._formbase() + '&reference='+id+'&value='+value;
 
-           new Ajax.Request(this._updateUrl, {method: 'post', parameters: formdata});
+           new Rico.ajaxRequest(this._updateUrl, {method: 'post', parameters: formdata});
        },
 
        _updateall: function(value) {
@@ -53,14 +64,14 @@ Rico.TableColumn.checkboxDB = Class.create(Rico.TableColumn.checkbox, {
                var cols = this.liveGrid.columns.length;
                for (var c = 0; c < cols; c++) {
                    var column = this.liveGrid.columns[c]
-                   if (column.filterType == Rico.TableColumn.USERFILTER) {
+                   if (column.filterType == Rico.ColumnConst.USERFILTER) {
                        var filter = column.filterOp + '|' + column.filterValues;
                        formdata += '&filter_'+c+'='+filter;
                    }
                }
            }
 
-           new Ajax.Request(this._updateUrl, {method: 'post', parameters: formdata});
+           new Rico.ajaxRequest(this._updateUrl, {method: 'post', parameters: formdata});
 
            var basename = this.liveGrid.tableId + '_chkbox_' + this.index + '_';
            var childCnt = this.numRows();
@@ -68,7 +79,7 @@ Rico.TableColumn.checkboxDB = Class.create(Rico.TableColumn.checkbox, {
            for (var r = 0; r < childCnt; r++)
                if (this.getValue(r) != null) {
                    var name = basename + r;
-                   $(name).checked = val;
+                   $('#'+name).prop('checked', val == 1);
                }
 
            var rows = this.liveGrid.buffer.totalRows;
@@ -84,15 +95,101 @@ Rico.TableColumn.checkboxDB = Class.create(Rico.TableColumn.checkbox, {
        },
 
        _createFilters: function(parent,name) {
-           field_p=RicoUtil.createFormField(parent,'img',null,name+'_p');
-           field_p.src = Rico.imgDir+'p.gif';
+           field_p=Rico.createFormField(parent,'img',null,name+'_p');
+           field_p.src = Hallinta.baseURL+'lib/rico/images/p.gif';
            field_p.tableColumn = this;
            field_p.onclick = function() { this.tableColumn._checkall(); };
-           field_m=RicoUtil.createFormField(parent,'img',null,name+'_m');
-           field_m.src = Rico.imgDir+'m.gif';
+           field_m=Rico.createFormField(parent,'img',null,name+'_m');
+           field_m.src = Hallinta.baseURL+'lib/rico/images/m.gif';
            field_m.tableColumn = this;
            field_m.onclick = function() { this.tableColumn._uncheckall(); };
        }
 });
 
-Rico.includeLoaded('ricoTableColumnDB.js');
+Rico.TableColumn.checkboxFunction = function(callback, callback_all) {
+    this.initialize(callback, callback_all);
+};
+
+Rico.TableColumn.checkboxFunction.prototype = Rico.extend(new Rico.TableColumn.checkbox(), {
+       initialize: function(callback, callback_all) {
+           this._checkedValue=1;
+           this._uncheckedValue=0
+           this._defaultValue=0
+           this._readOnly=0
+           this._checkboxes=[];
+
+           this._callback = callback;
+           this._callback_all = callback_all;
+       },
+
+       _onclick: function(e) {
+           var elem=Rico.eventElement(e);
+           var windowRow=parseInt(elem.id.substr((elem.id.lastIndexOf('_',elem.id.length)+1)));  //faster than split
+           var newval=elem.checked ? this._checkedValue : this._uncheckedValue;
+           this.setValue(windowRow,newval);
+
+           this._update(windowRow);
+       },
+
+       _formbase: function() {
+           return 'table='+this.liveGrid.tableId+'&column='+this.index;
+       },
+
+       _update: function(row) {
+            var value = this.getValue(row);
+
+           if (value == undefined)
+               return false;
+
+           this._callback(this, row, value);
+       },
+
+       _updateall: function(value) {
+           var formdata = 'value='+value;
+
+           // Warning: filters not detected in subgrid
+           if (this.liveGrid.filterCount() > 0) {
+               var cols = this.liveGrid.columns.length;
+               for (var c = 0; c < cols; c++) {
+                   var column = this.liveGrid.columns[c]
+                   if (column.filterType == Rico.ColumnConst.USERFILTER) {
+                       var filter = column.filterOp + '|' + column.filterValues;
+                       formdata += '&filter_'+c+'='+filter;
+                   }
+               }
+           }
+
+           this._callback_all(formdata);
+
+           var basename = this.liveGrid.tableId + '_chkbox_' + this.index + '_';
+           var childCnt = this.numRows();
+           var val = value?true:false;
+           for (var r = 0; r < childCnt; r++)
+               if (this.getValue(r) != null) {
+                   var name = basename + r;
+                   $('#'+name).prop('checked', val == 1);
+               }
+
+           var rows = this.liveGrid.buffer.totalRows;
+           for (var i = 0; i < rows; i++)
+               this.liveGrid.buffer.setValue(i, this.index, value);
+       },
+
+       _checkall: function() {
+           this._updateall(1);
+       },
+       _uncheckall: function() {
+           this._updateall(0);
+       },
+
+       _createFilters: function(parent,name) {
+           field_p=Rico.createFormField(parent,'img',null,name+'_p');
+           field_p.src = Hallinta.baseURL+'lib/rico/images/p.gif';
+           field_p.tableColumn = this;
+           field_p.onclick = function() { this.tableColumn._checkall(); };
+           field_m=Rico.createFormField(parent,'img',null,name+'_m');
+           field_m.src = Hallinta.baseURL+'lib/rico/images/m.gif';
+           field_m.tableColumn = this;
+           field_m.onclick = function() { this.tableColumn._uncheckall(); };
+       }
+});
index 7ef7659..7538697 100644 (file)
@@ -12,6 +12,7 @@ $mask
   join = string
   rows = Integer
   sort = Integer
+  sortdir = ASC | DESC
   maxprint = Integer (-> maxPrint, max exportable rows in table)
   prefetch = true / false (default true)
   html = string
diff --git a/masks/dates/send-reminder b/masks/dates/send-reminder
new file mode 100755 (executable)
index 0000000..7eb0308
--- /dev/null
@@ -0,0 +1,85 @@
+#! /usr/bin/perl
+
+use DBI;
+use Encode;
+use Getopt::Long;
+use Mail::Sendmail;
+use Date::Calc qw/Day_of_Week/;
+
+my $cfg;
+my $db;
+
+sub basepath
+{
+    my @parts = split('/', $0);
+    pop @parts;pop @parts;pop @parts;
+    return join('/', @parts);
+}
+
+sub config_read
+{
+    my $info;
+    open my $f, basepath.'/config.php' or return;
+    while (<$f>) {
+       chomp;
+       next if /^#/ || /^\s*$/;
+       next unless /^\s*define\(['"](.*?)["']\s*,\s*['"](.*?)["']\)/;
+       $info->{$1} = $2;
+    }
+    close $f;
+    return $info;
+}
+
+sub dbconnect
+{
+    my $dsn = 'dbi:' . ($cfg->{DBDRIVER} eq 'pgsql' ? 'Pg:dbname=' : 'mysql:database=') . $cfg->{DBNAME};
+    $dsn .= ';hostname='.$cfg->{DBHOST} if $cfg->{DBHOST} ne 'localhost';
+    my $dbh = DBI->connect($dsn, $cfg->{DBUSER}, $cfg->{DBPASS},
+                          {'PrintError' => 1,
+                           'RaiseError' => 0}) or die "Can't connect to database: $DBI::errstr\n";
+    return $dbh;
+}
+
+sub fetch_dates
+{
+    my $delta = $cfg->{DATES_DELTA};
+    $delta = 1 unless defined $delta;
+
+    my $sql = sprintf("SELECT date_dates.id,date,name FROM date_dates JOIN date_types ON type = date_types.id " .
+                     "WHERE notify = 1 AND date = now()::date + interval '%d days' ORDER BY name",
+                     $delta);
+    my $sth = $db->prepare($sql);
+    $sth->execute;
+    return $sth->fetchall_hashref('id');
+}
+
+sub send_reminder
+{
+    my $dates = shift;
+    my $body;
+    my @datenames;
+
+    my @wdays = qw/Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag/;
+    foreach my $row (values %$dates) {
+       my ($year,$month,$day) = split(/-/, $row->{date});
+       my $dow = $wdays[Day_of_Week($year,$month,$day)];
+       $body .= sprintf("\n%-10s  %s  %s\n", $dow, $row->{date}, $row->{name});
+       push @datenames, $row->{name};
+    }
+
+    my %mail = ('To' => $cfg->{DATES_REMINDER},
+                'From' => sprintf('%s <%s>', $cfg->{TITLE}, $cfg->{MAIL_FROM}),
+                'Subject' => encode("MIME-Header", decode('UTF-8', join(', ', @datenames))),
+                'Content-type' => 'text/plain; charset=UTF-8',
+               'Content-Transfer-Encoding' => '8bit',
+                'Message' => $body,
+        );
+
+    sendmail(%mail) or die $Mail::Sendmail::error;
+}
+
+$cfg = config_read;
+die "Missing define for DATES_REMINDER" unless $cfg->{DATES_REMINDER};
+$db = dbconnect;
+my $dates = fetch_dates;
+send_reminder $dates if keys %$dates;
diff --git a/masks/documents/download.php b/masks/documents/download.php
new file mode 100644 (file)
index 0000000..a50269a
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+
+define('MODULE', 'documents');
+
+$href = sprintf("new Rico.TableColumn.link('%s%s','_new')",
+               Hallinta::instance()->urlbase(),
+               'ajax/ajax.php?id={0}&source=documents__files&func=file&name=download');
+
+JavaScript::instance()->file('lib/ricoTableColumnDB.js');
+
+Actions::instance()->addLink(new Link(array('id' => 'btn_clear',
+                                           'icon' => Hallinta::instance()->urlbase().'images/icons/clean.png',
+                                           'title' => 'Clear selection',
+                                           'function' => 'clear_selection')));
+Actions::instance()->addLink(new Link(array('id' => 'btn_archive',
+                                           'icon' => Hallinta::instance()->urlbase().'images/icons/download16.png',
+                                           'title' => 'Download as ZIP',
+                                           'function' => 'name_popup_open')));
+
+$jscode = <<<EOC
+var name_popup = false;
+function name_popup_open()
+{
+    if (!name_popup) {
+       var options = { hideOnClick: false, canDragFunc: true };
+
+       name_popup = new Rico.Window('Archiv-Name', options);
+
+       load_template('popup/filename', {}, function(name, data){
+               name_popup.contentDiv.innerHTML = data;
+               $('#form_name').select();
+       });
+
+       name_popup.centerPopup();
+    } else {
+       name_popup.openPopup();
+       $('#form_name').select();
+    }
+
+    return false;
+}
+
+function build_zip(obj)
+{
+    if (!$('#form_name').val().length) return false;
+
+    name_popup.closePopup();
+    Hallinta.showMsg('Bitte warten, das Archiv wird erstellt...');
+
+    ajax_request('function', 'callback=archive&name='+encodeURIComponent($('#form_name').val()), function(data){
+       Hallinta.hideMsg();
+       info('Archiv erstellt');
+       window.open(Hallinta.baseURL + 'ajax/ajax.php?source='+Hallinta.pageSource+'&func=file&name=archive','_blank');
+    });
+
+    return false;
+}
+
+function clear_selection()
+{
+  ajax_request('function', 'callback=clear', function(data){
+      info('Auswahl geleert');
+      Hallinta.showMsg('Auswahl geleert', {timeout: 3});
+
+      grid_update(Hallinta.grid);
+    });
+
+  return false;
+}
+
+EOC;
+
+JavaScript::instance()->add($jscode);
+
+$sql_download = "
+CASE
+WHEN substring(lower(path),length(path)-3+1) = 'png' THEN '<img src=\"IMGPATH/png.png\" title=\"Download PNG\">'
+WHEN substring(lower(path),length(path)-3+1) = 'pdf' THEN '<img src=\"IMGPATH/pdf.gif\" title=\"Download PDF\">'
+WHEN substring(lower(path),length(path)-3+1) = 'zip' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Zip\">'
+WHEN substring(lower(path),length(path)-3+1) = 'csv' THEN '<img src=\"IMGPATH/csv.gif\" title=\"Download CSV \">'
+WHEN substring(lower(path),length(path)-3+1) = 'odt' THEN '<img src=\"IMGPATH/odt.png\" title=\"Download Odt\">'
+WHEN substring(lower(path),length(path)-3+1) = 'doc' THEN '<img src=\"IMGPATH/doc.gif\" title=\"Download Doc\">'
+WHEN substring(lower(path),length(path)-4+1) = 'docx' THEN '<img src=\"IMGPATH/doc.gif\" title=\"Download Doc\">'
+WHEN substring(lower(path),length(path)-3+1) = 'xls' THEN '<img src=\"IMGPATH/xls.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-4+1) = 'xlsx' THEN '<img src=\"IMGPATH/xls.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-3+1) = 'ods' THEN '<img src=\"IMGPATH/table.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-8+1) = 'gnumeric' THEN '<img src=\"IMGPATH/gnumeric.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-6+1) = 'tar.gz' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Tar\">'
+WHEN substring(lower(path),length(path)-7+1) = 'tar.bz2' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Tar\">'
+WHEN substring(lower(path),length(path)-3+1) = 'gif' THEN '<img src=\"IMGPATH/image.gif\" title=\"Download image\">'
+WHEN substring(lower(path),length(path)-3+1) = 'tex' THEN '<img src=\"IMGPATH/tex.png\" title=\"Download TeX file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'sql' THEN '<img src=\"IMGPATH/sql.png\" title=\"Download SQL file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'txt' THEN '<img src=\"IMGPATH/text.png\" title=\"Download text file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'jpg' THEN '<img src=\"IMGPATH/graphic.gif\" title=\"Display image\">'
+WHEN substring(lower(path),length(path)-4+1) = 'epub' THEN '<img src=\"IMGPATH/ebook.gif\" title=\"Download e-book\">'
+WHEN substring(lower(path),length(path)-4+1) = 'html' THEN '<img src=\"IMGPATH/html.png\" title=\"Display HTML\">'
+ELSE '<img src=\"IMGPATH/download.png\" title=\"download\">'
+END
+";
+
+$sql_download = str_replace('IMGPATH', Hallinta::instance()->urlbase().'images/icons', $sql_download);
+
+$mask = array(
+             'table' => 'doc_files',
+             'title' => 'Dokumente',
+             'list' => array(
+                             'id' => array(
+                                           'name' => 'ID',
+                                           'visible' => false,
+                                           ),
+                             'owner' => array(
+                                           'name' => 'Besitzer',
+                                           'sqltype' => 'int',
+                                           'width' => 90,
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('doc_owner','id','name').", 0, '')",
+                                           'distinct' => "SELECT DISTINCT owner,name FROM doc_files JOIN doc_owner ON owner = doc_owner.id ORDER BY name",
+                                           ),
+                             'type' => array(
+                                           'name' => 'Typ',
+                                           'sqltype' => 'int',
+                                           'width' => 90,
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('doc_types','id','name').", 0, '')",
+                                           'distinct' => "SELECT DISTINCT type,name FROM doc_files JOIN doc_types ON type = doc_types.id ORDER BY name",
+                                           ),
+                             'extract(year from date)' => array(
+                                           'name' => 'Jahr',
+                                           'sqltype' => 'int',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           'filter' => 's',
+                                           'width' => 50,
+                                           'distinct' => "SELECT DISTINCT extract(year from date) AS year FROM doc_files ORDER BY year DESC",
+                                           ),
+                             'date' => array(
+                                           'name' => 'Datum',
+                                           'sqltype' => 'date',
+                                           'filter' => 't^8',
+                                           'width' => 85,
+                                           ),
+                             'title' => array(
+                                           'name' => 'Titel',
+                                           'width' => 345,
+                                           'filter' => 't',
+                                           ),
+                             'path' => array(
+                                           'name' => 'Pfad',
+                                           'width' => 250,
+                                           'filter' => 't',
+                                           'visible' => false,
+                                           ),
+                             'download' => array(
+                                           'name' => 'DL',
+                                           'width' => 25,
+                                           'specs' => array('ClassName' => 'aligncenter', 'canSort' => false),
+                                           'control' => $href,
+                                           'sql' => $sql_download,
+                                           ),
+                             'selected' => array(
+                                           'name' => 'Sel',
+                                           'width' => 40,
+                                           'sql' => '(SELECT count(*) FROM doc_files_selection WHERE user_ = '
+                                               . intval($_SESSION['sys']['uid'])
+                                               . ' AND file = doc_files.id)',
+                                           'control' => "new Rico.TableColumn.checkboxDB(0, 'ajax/ricoUpdateConnection.php')",
+                                           'filter' => 'c',
+                                           'specs' => array('ClassName' => 'aligncenter has-checkbox', 'canSort' => false),
+                                           'update' => array('table' => 'doc_files_selection',
+                                                             'basecol' => 'user_',
+                                                             'baseval' => $_SESSION['sys']['uid'],
+                                                             'refcol' => 'file',
+                                                             'reftable' => 'doc_files',
+                                                             'refid' => 'doc_files.id',
+                                                             ),
+                                           ),
+                             ),
+             'files' => array(
+                              'download' => process_download,
+                              'archive' => process_archive,
+                              ),
+             'callbacks' => array(
+                                  'clear' => cb_clear,
+                                  'archive' => cb_archive,
+                               ),
+             );
+
+function mk_tempdir()
+{
+  $tmpdir = sprintf("%sarchive/%s/%s",
+                   $_SESSION['sys']['basedir'],
+                   MODULE,
+                   md5(date('c')));
+  mkdir($tmpdir,0777);
+
+  return $tmpdir;
+}
+
+function rm_tempdir($dir)
+{
+  system(sprintf("rm -rf %s", $dir));
+}
+
+function process_download()
+{
+  global $mask;
+
+  download_file($mask['table'],'path',MODULE,$_GET['id']);
+}
+
+function process_archive()
+{
+  $fname = $_SESSION['files_zipfile'];
+
+  if (!file_exists($fname) || !is_file($fname))
+    return 'File not found';
+
+  send_file($fname);
+  rm_tempdir($_SESSION['files_tmpdir']);
+
+  unset($_SESSION['files_tmpdir']);
+  unset($_SESSION['files_zipfile']);
+}
+
+function cb_clear()
+{
+  global $db;
+
+  return $db->execute(sprintf("DELETE FROM doc_files_selection WHERE user_ = %d", $_SESSION['sys']['uid']));
+}
+
+function cb_archive()
+{
+  global $db;
+
+  $sql = sprintf("SELECT date,title,path FROM doc_files JOIN doc_files_selection ON file = doc_files.id WHERE user_ = %d ORDER BY date, doc_files.id",
+                $_SESSION['sys']['uid']);
+
+  $tmpdir = mk_tempdir();
+
+  chdir($tmpdir.'/..');
+
+  $zipfile = $tmpdir.'/'.$_POST['name'].'.zip';
+  $cmd = sprintf("zip -q -u --names-stdin %s", escapeshellarg($zipfile));
+
+error_log($cmd);
+  $p = popen($cmd, 'w');
+  if (!$p) return array('error' => 'Kann ZIP nicht aufrufen');
+
+  $text = '';
+  foreach ($db->fetchObjectList($sql) as $row) {
+    fwrite($p, $row->path . "\n");
+    $text .= sprintf("%s %-60s %s\n", $row->date, $row->title, $row->path);
+  }
+
+  if (strlen($text))
+    $text = sprintf("%s %-60s %s\n", 'Date', 'Description', 'Path') . $text;
+
+  $f = fopen('content.txt', 'w');
+  if ($f !== false) {
+    fwrite($f, $text);
+    fclose($f);
+    fwrite($p, "content.txt\n");
+  }
+  pclose($p);
+  if (file_exists('content.txt'))
+    unlink('content.txt');
+
+  $_SESSION['files_tmpdir'] = $tmpdir;
+  $_SESSION['files_zipfile'] = $zipfile;
+
+  return true;
+}
index 784d7e0..d32d139 100644 (file)
 <?php
 
-$href = "new Rico.TableColumn.link('ajax/ajax.php?id={0}&source=documents__files&func=file&name=download','_new')";
+if (file_exists(__DIR__.'/files_config.php'))
+    # scanadf --list-devices
+    require_once(__DIR__.'/files_config.php');
+
+$href = sprintf("new Rico.TableColumn.link('%s%s','_new')",
+               Hallinta::instance()->urlbase(),
+               'ajax/ajax.php?id={0}&source=documents__files&func=file&name=download');
+
+JavaScript::instance()->add("Hallinta.closeDialogAfterInsert = false;");
+JavaScript::instance()->add("Hallinta.fetchItemAfterInsert = true;");
+JavaScript::instance()->add("Hallinta.preInsert = check_insert_file;");
+JavaScript::instance()->add("Hallinta.postFetch = files_post_fetch;");
+
+if (basename($_SERVER['SCRIPT_NAME']) == 'index.php' && ScannerBase::getScanner()->hasScanner()) {
+    $icon = Hallinta::instance()->urlbase().'masks/'.basename(__DIR__).'/icon/scanner.png';
+    Actions::instance()->addLink(new Link(array('id' => 'btn_scan',
+                                               'icon' => $icon,
+                                               'title' => 'Open Scanner',
+                                               'function' => 'open_scanner')));
+}
+
+$jscode = <<<EOC
+var files_scanner_popup = false;
+var files_scanner_close_popup = false;
+var files_scanner_arrange_popup = false;
+var files_scanner_saving = false;
+function check_insert_file()
+{
+    if (files_scanner_popup && files_scanner_popup.visible() && !\$('#edit_id').val().length)
+       return true;
+
+    if (!\$('#edit_path').val().length) {
+       Hallinta.showMsg('Bitte eine Datei auswählen', {timeout: 5});
+       error('File missing');
+       return false;
+    }
+
+    return true;
+}
+
+function scanner_url(type)
+{
+    if (type == 'download')
+       var url = Hallinta.baseURL + 'ajax/ajax.php?func=file&name=download&source=' + Hallinta.pageSource;
+    else if (type == 'scanner')
+       var url = Hallinta.baseURL + 'ajax/ajax.php?func=file&name=scanner&source=' + Hallinta.pageSource;
+    else
+       var url = Hallinta.baseURL + 'ajax/ajax.php?func=file&name=preview&source=' + Hallinta.pageSource;
+
+    return url;
+}
+
+function close_scanner()
+{
+    if (files_scanner_close_popup) {
+       files_scanner_close_popup.openPopup();
+    } else {
+       files_scanner_close_popup = new Rico.Window('Scanner');
+       ajax_request('template', 'template=popup/close', function(data){
+           \$(files_scanner_close_popup.contentDiv).html(data);
+           files_scanner_close_popup.centerPopup();
+       });
+    }
+}
+
+function open_scanner()
+{
+    Hallinta.editDialog.closePopup();
+
+    if (files_scanner_popup) {
+       files_scanner_popup.openPopup();
+    } else {
+       files_scanner_popup = new Rico.Window('Scanner', {onClose: close_scanner});
+       ajax_callback('scanner', null, function(data){
+           \$(files_scanner_popup.contentDiv).html(data.html);
+           \$('#preview').css('max-width', (window.innerWidth-35) + 'px')
+           \$('#preview').css('max-height', (window.innerHeight-110) + 'px')
+           \$('#preview_img').on('load', function(){
+               files_scanner_popup.centerPopup();
+           });
+           if (data.preview) {
+               \$('#preview_img').attr('src', scanner_url('preview')).show();
+           } else {
+               files_scanner_popup.centerPopup();
+           }
+       });
+    }
+}
+
+function scanner_preview()
+{
+    Hallinta.showMsg("Bitte warten, das Dokument wird gescannt...");
+    ajax_callback('preview', null, function(data){
+       var time = (new Date()).toUTCString();
+       \$('#preview_img').attr('src', scanner_url('preview')+'&'+time).show();
+       files_scanner_popup.centerPopup();
+       Hallinta.hideMsg();
+    });
+}
+
+function scanner_rotate(direction)
+{
+    Hallinta.showMsg("Bitte warten, die Vorschau wird gedreht...");
+    ajax_callback('rotate', 'direction='+direction, function(data){
+       var time = (new Date()).toUTCString();
+       \$('#preview_img').attr('src', scanner_url('preview')+'&'+time).show();
+       files_scanner_popup.centerPopup();
+       Hallinta.hideMsg();
+    });
+}
+
+function scanner_download()
+{
+    Hallinta.showMsg("Bitte warten, die Datei wird vorbereitet...");
+    ajax_callback('download', null, function(data){
+       Hallinta.hideMsg();
+       window.location.href = scanner_url('scanner');
+    });
+}
+
+function files_post_fetch(data)
+{
+    if (files_scanner_saving) {
+       Hallinta.editDialog.closePopup();
+       files_scanner_saving = false;
+
+       set_value('edit_id', '');
+       set_value('edit_path', '');
+       set_value('edit_path_source', '');
+       set_value('edit_real_path', '');
+    }
+}
+
+function scanner_save()
+{
+    files_scanner_saving = true;
+    Hallinta.openEditDialog();
+    button_enable('button_insert');
+    set_value('edit_id', '');
+    set_value('edit_path', '');
+    set_value('edit_path_source', 'scanner');
+    set_value('edit_real_path', 'Dokument vom Scanner');
+}
+
+function scanner_tidy()
+{
+    ajax_callback('tidy', null, function(data){
+       files_scanner_close_popup.closePopup();
+    });
+}
+
+function scanner_store()
+{
+    ajax_callback('store', null, function(data){
+       Hallinta.showMsg("Scan als Seite " + data.page + " gespeichert.", {timeout: 2});
+       ajax_callback('peep', {name: data.name});
+    });
+}
+
+function close_arrange()
+{
+    if (files_scanner_close_popup) {
+       files_scanner_close_popup.openPopup();
+    } else {
+       files_scanner_close_popup = new Rico.Window('Scanner');
+       ajax_request('template', 'template=popup/close', function(data){
+           \$(files_scanner_close_popup.contentDiv).html(data);
+           files_scanner_close_popup.centerPopup();
+       });
+    }
+}
+
+function load_arrange_content()
+{
+    ajax_callback('arrange', null, function(data){
+       \$(files_scanner_arrange_popup.contentDiv).html(data.html);
+       \$('#spreview').css('max-width', (window.innerWidth-35) + 'px')
+       \$('#spreview').css('max-height', (window.innerHeight-110) + 'px')
+       \$('#spreview img.peep').on('load', function(){
+           \$(this).parents('div.page').show();
+           files_scanner_arrange_popup.centerPopup();
+       });
+
+       files_scanner_arrange_popup.centerPopup();
+    });
+}
+
+function open_arrange()
+{
+    if (files_scanner_arrange_popup) {
+       files_scanner_arrange_popup.openPopup();
+    } else {
+       files_scanner_arrange_popup = new Rico.Window('Anordnen', {zIndex: 4});
+    }
+
+    load_arrange_content();
+}
+
+function arrangement_download()
+{
+    Hallinta.showMsg("Bitte warten, die Datei wird vorbereitet...");
+    ajax_callback('series', null, function(data){
+       Hallinta.hideMsg();
+       window.location.href = scanner_url('scanner')+'&series=1';
+    });
+}
+
+function arrangement_up(o)
+{
+    ajax_callback('arrange_up', 'name='+\$(o).parents('div.page').data('name'), function(data){
+       load_arrange_content();
+    });
+
+    return false;
+}
+
+function arrangement_down(o)
+{
+    ajax_callback('arrange_down', 'name='+\$(o).parents('div.page').data('name'), function(data){
+       load_arrange_content();
+    });
+
+    return false;
+}
+
+function arrangement_del(o)
+{
+    ajax_callback('arrange_del', 'name='+\$(o).parents('div.page').data('name'), function(data){
+       load_arrange_content();
+    });
+
+    return false;
+}
+
+function arrangement_save()
+{
+    Hallinta.openEditDialog();
+    button_enable('button_insert');
+    set_value('edit_id', '');
+    set_value('edit_path', '');
+    set_value('edit_path_source', 'series');
+    set_value('edit_real_path', 'Mehrseitiges Dokument vom Scanner');
+}
+
+function arrangement_tidy()
+{
+    ajax_callback('series_tidy', null, function(data){
+       files_scanner_arrange_popup.closePopup();
+    });
+}
+
+EOC;
+
+JavaScript::instance()->add($jscode);
 
 $sql_download = "
 CASE
-WHEN substring(lower(path),length(path)-3+1) = 'png' THEN '<img src=\"images/icons/png.png\" title=\"Download PNG\">'
-WHEN substring(lower(path),length(path)-3+1) = 'pdf' THEN '<img src=\"images/icons/pdf.gif\" title=\"Download PDF\">'
-WHEN substring(lower(path),length(path)-3+1) = 'zip' THEN '<img src=\"images/icons/zip.gif\" title=\"Download Zip\">'
-WHEN substring(lower(path),length(path)-3+1) = 'odt' THEN '<img src=\"images/icons/odt.png\" title=\"Download Odt\">'
-WHEN substring(lower(path),length(path)-3+1) = 'doc' THEN '<img src=\"images/icons/doc.gif\" title=\"Download Doc\">'
-WHEN substring(lower(path),length(path)-4+1) = 'docx' THEN '<img src=\"images/icons/doc.gif\" title=\"Download Doc\">'
-WHEN substring(lower(path),length(path)-3+1) = 'xls' THEN '<img src=\"images/icons/xls.png\" title=\"Download spreadsheet\">'
-WHEN substring(lower(path),length(path)-4+1) = 'xlsx' THEN '<img src=\"images/icons/xls.png\" title=\"Download spreadsheet\">'
-WHEN substring(lower(path),length(path)-3+1) = 'ods' THEN '<img src=\"images/icons/table.png\" title=\"Download spreadsheet\">'
-WHEN substring(lower(path),length(path)-6+1) = 'tar.gz' THEN '<img src=\"images/icons/zip.gif\" title=\"Download Tar\">'
-WHEN substring(lower(path),length(path)-7+1) = 'tar.bz2' THEN '<img src=\"images/icons/zip.gif\" title=\"Download Tar\">'
-WHEN substring(lower(path),length(path)-3+1) = 'gif' THEN '<img src=\"images/icons/image.gif\" title=\"Download image\">'
-WHEN substring(lower(path),length(path)-3+1) = 'tex' THEN '<img src=\"images/icons/tex.png\" title=\"Download TeX file\">'
-WHEN substring(lower(path),length(path)-3+1) = 'sql' THEN '<img src=\"images/icons/sql.png\" title=\"Download SQL file\">'
-WHEN substring(lower(path),length(path)-3+1) = 'txt' THEN '<img src=\"images/icons/text.png\" title=\"Download text file\">'
-ELSE '<img src=\"images/icons/download.gif\" title=\"download\">'
+WHEN substring(lower(path),length(path)-3+1) = 'png' THEN '<img src=\"IMGPATH/png.png\" title=\"Download PNG\">'
+WHEN substring(lower(path),length(path)-3+1) = 'pdf' THEN '<img src=\"IMGPATH/pdf.gif\" title=\"Download PDF\">'
+WHEN substring(lower(path),length(path)-3+1) = 'zip' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Zip\">'
+WHEN substring(lower(path),length(path)-2+1) = '7z' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download 7-Zip\">'
+WHEN substring(lower(path),length(path)-3+1) = 'csv' THEN '<img src=\"IMGPATH/csv.gif\" title=\"Download CSV \">'
+WHEN substring(lower(path),length(path)-3+1) = 'odt' THEN '<img src=\"IMGPATH/odt.png\" title=\"Download Odt\">'
+WHEN substring(lower(path),length(path)-3+1) = 'doc' THEN '<img src=\"IMGPATH/doc.gif\" title=\"Download Doc\">'
+WHEN substring(lower(path),length(path)-4+1) = 'docx' THEN '<img src=\"IMGPATH/doc.gif\" title=\"Download Doc\">'
+WHEN substring(lower(path),length(path)-3+1) = 'xls' THEN '<img src=\"IMGPATH/xls.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-4+1) = 'xlsx' THEN '<img src=\"IMGPATH/xls.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-3+1) = 'ods' THEN '<img src=\"IMGPATH/table.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-8+1) = 'gnumeric' THEN '<img src=\"IMGPATH/gnumeric.png\" title=\"Download spreadsheet\">'
+WHEN substring(lower(path),length(path)-6+1) = 'tar.gz' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Tar\">'
+WHEN substring(lower(path),length(path)-7+1) = 'tar.bz2' THEN '<img src=\"IMGPATH/zip.gif\" title=\"Download Tar\">'
+WHEN substring(lower(path),length(path)-3+1) = 'gif' THEN '<img src=\"IMGPATH/image.gif\" title=\"Download image\">'
+WHEN substring(lower(path),length(path)-3+1) = 'tex' THEN '<img src=\"IMGPATH/tex.png\" title=\"Download TeX file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'sql' THEN '<img src=\"IMGPATH/sql.png\" title=\"Download SQL file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'txt' THEN '<img src=\"IMGPATH/text.png\" title=\"Download text file\">'
+WHEN substring(lower(path),length(path)-3+1) = 'jpg' THEN '<img src=\"IMGPATH/graphic.gif\" title=\"Display image\">'
+WHEN substring(lower(path),length(path)-4+1) = 'epub' THEN '<img src=\"IMGPATH/ebook.gif\" title=\"Download e-book\">'
+WHEN substring(lower(path),length(path)-4+1) = 'html' THEN '<img src=\"IMGPATH/html.png\" title=\"Display HTML\">'
+ELSE '<img src=\"IMGPATH/download.png\" title=\"download\">'
 END
 ";
 
+$sql_download = str_replace('IMGPATH', Hallinta::instance()->urlbase().'images/icons', $sql_download);
+
+$monthlist = ['1' => 'Januar',
+             '2' => 'Februar',
+             '3' => 'März',
+             '4' => 'April',
+             '5' => 'Mai',
+             '6' => 'Juni',
+             '7' => 'Juli',
+             '8' => 'August',
+             '9' => 'September',
+             '10' => 'Oktober',
+             '11' => 'November',
+             '12' => 'Dezember'];
+
 $mask = array(
              'table' => 'doc_files',
              'title' => 'Dokumente',
+             'sort' => 1,
+             'sortdir' => 'DESC',
              'list' => array(
                              'id' => array(
                                            'name' => 'ID',
                                            'visible' => false,
                                            ),
+                             'date||\'-\'||lpad(id::text, 5, \'0\')' => array(
+                                           'name' => 'SortCol',
+                                           'visible' => false,
+                                           ),
                              'owner' => array(
                                            'name' => 'Besitzer',
+                                           'sqltype' => 'int',
                                            'width' => 90,
-                                           'specs' => "ClassName: 'aligncenter', filterUI: 's'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('doc_owner','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT owner,name FROM doc_files JOIN doc_owner ON owner = doc_owner.id ORDER BY name",
                                            ),
                              'type' => array(
                                            'name' => 'Typ',
+                                           'sqltype' => 'int',
                                            'width' => 90,
-                                           'specs' => "ClassName: 'aligncenter', filterUI: 's'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('doc_types','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT type,name FROM doc_files JOIN doc_types ON type = doc_types.id ORDER BY name",
                                            ),
+                             'extract(year from date)' => array(
+                                           'name' => 'Jahr',
+                                           'sqltype' => 'int',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           'filter' => 's',
+                                           'width' => 50,
+                                           'distinct' => "SELECT DISTINCT extract(year from date) AS year FROM doc_files ORDER BY year DESC",
+                                           ),
+                             'extract(month from date)' => array(
+                                           'name' => 'Monat',
+                                           'sqltype' => 'int',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           'filter' => 's',
+                                           'width' => 50,
+                                           'control' => "new Rico.TableColumn.lookup(".json_encode($monthlist).", 0, '')",
+                                           'distinct' => "SELECT DISTINCT extract(month from date) AS month FROM doc_files ORDER BY month ASC",
+                                           ),
                              'date' => array(
                                            'name' => 'Datum',
-                                           'width' => 85,
+                                           'sqltype' => 'date',
+                                           'filter' => 't^5',
+                                           'width' => 80,
                                            ),
                              'title' => array(
                                            'name' => 'Titel',
                                            'width' => 345,
-                                           'specs' => "filterUI: 't'",
+                                           'filter' => 't',
+                                           ),
+                             'filename' => array(
+                                           'name' => 'Datei',
+                                           'width' => 200,
+                                           'filter' => 't',
+                                           'visible' => false,
+                                           'sql' => "regexp_replace(path, '.+/', '')",
+                                           ),
+                             'path' => array(
+                                           'name' => 'Pfad',
+                                           'width' => 300,
+                                           'filter' => 't',
+                                           'visible' => false,
                                            ),
                              'download' => array(
                                            'name' => 'DL',
                                            'width' => 25,
-                                           'specs' => "ClassName: 'aligncenter', canSort: false",
+                                           'specs' => array('ClassName' => 'aligncenter', 'canSort' => false),
                                            'control' => $href,
                                            'sql' => $sql_download,
                                            ),
@@ -94,7 +410,14 @@ $mask = array(
                                            'name' => 'Dokument',
                                            'type' => 'file',
                                            'path' => 'documents',
+                                           'source' => 'use_from_scanner',
+                                           'sql' => false,
+                                           ),
+                             'path_source' => array(
+                                           'name' => 'Dokument Source',
+                                           'type' => 'hidden',
                                            'sql' => false,
+                                           'ignore' => true,
                                            ),
                              'keywords' => array(
                                            'name' => 'Schlagworte',
@@ -102,15 +425,413 @@ $mask = array(
                                            ),
                              ),
              'files' => array(
-                              'download' => process_download,
+                              'download' => 'process_download',
+                              'preview' => 'process_preview',
+                              'scanner' => 'process_scanner',
                               ),
+             'delete' => 'delete_file',
              );
 
 function process_download()
 {
   global $mask;
 
-  download_file($mask['table'],'path',$mask['edit']['path']['path'],$_GET['id']);
+  if ($_SESSION['sys']['login'] == 'joey')
+      $inline = true;
+  else
+      $inline = false;
+
+  download_file($mask['table'],'path',$mask['edit']['path']['path'],$_GET['id'],$inline);
+}
+
+function process_preview()
+{
+    if (isset($_GET['id'])) {
+       assert_peep_image($_GET['id']);
+       $path = get_scandir() . $_SESSION['sys']['login'] . '.' . $_GET['id'] . '.png';
+    } else {
+       $path = Hallinta::instance()->basedir() . get_preview_png();
+    }
+    send_file($path, true);
+}
+
+function process_scanner()
+{
+    if (isset($_GET['series']))
+       $preview_pdf = 'archive/documents/scanner/'.$_SESSION['sys']['login'].'.series.pdf';
+    else
+       $preview_pdf = 'archive/documents/scanner/'.$_SESSION['sys']['login'].'.pdf';
+
+    send_file(Hallinta::instance()->basedir() . $preview_pdf, false);
+}
+
+function delete_file()
+{
+  global $db;
+
+  $sql = sprintf("SELECT path FROM doc_files WHERE id = %d", $_POST['id']);
+  $sth = $db->query($sql);
+  $row = $sth->fetch();
+
+  if (unlink($_SESSION['sys']['basedir'] . 'archive/documents/'.$row['path']) === false)
+    return array('error' => 'Cannot delete file');
+
+  $sql = sprintf("DELETE FROM doc_files WHERE id = %d", $_POST['id']);
+  $db->query($sql);
+
+  return array('status' => true);
+}
+
+/*
+ * Convenience functions:
+ *   get_scandir()
+ *   get_preview_pnm()
+ *   get_preview_png()
+ *   get_series_file()
+ */
+function get_scandir()
+{
+    return Hallinta::instance()->basedir() . 'archive/documents/scanner/';
+}
+
+function get_preview_pnm()
+{
+    return get_scandir() . $_SESSION['sys']['login'] . '.pnm';
+}
+
+function get_preview_png()
+{
+    return 'archive/documents/scanner/'.$_SESSION['sys']['login'].'.png';
+}
+
+function get_series_file()
+{
+    return get_scandir() . $_SESSION['sys']['login'] . '.series';
+}
+
+function pnm2pdf()
+{
+    $scanner = ScannerBase::getScanner();
+
+    // TODO: Use arranged pages
+    $preview_pnm = get_preview_pnm();
+    $preview_pdf = 'archive/documents/scanner/'.$_SESSION['sys']['login'].'.pdf';
+
+    $scanner->convertPNMtoPDF($preview_pnm, Hallinta::instance()->basedir() . $preview_pdf);
+
+    return $preview_pdf;
+}
+
+/*
+ * Series processing:
+ *   series_tidy();
+ *   series_load();
+ *   series_save(Array $list);
+ *   series_append();
+ *   assert_peep_image($name);
+ *   assert_peep_pdf($name);
+ */
+function series_load()
+{
+    $series = get_series_file();
+
+    if (!file_exists($series))
+       return [];
+
+    $list = [];
+    foreach (explode("\n", file_get_contents($series)) as $name)
+       if (strlen($name))
+           $list[] = $name;
+
+    return $list;
+}
+
+function series_tidy()
+{
+    $dir = get_scandir();
+    $list = series_load();
+
+    foreach ($list as $name) {
+       $path = $dir.$_SESSION['sys']['login'].'.'.$name.'.pnm';
+       if (file_exists($path)) unlink($path);
+       $path = $dir.$_SESSION['sys']['login'].'.'.$name.'.png';
+       if (file_exists($path)) unlink($path);
+    }
+
+    series_save([]);
+}
+
+function series_save(Array $list)
+{
+    $series = get_series_file();
+
+    if (file_exists($series))
+       unlink($series);
+
+    $content = "";
+    foreach ($list as $name)
+       if ($name)
+           $content .= $name . "\n";
+
+    file_put_contents($series, $content);
+}
+
+function series_append()
+{
+    $list = series_load();
+
+    $name = md5(time());
+    $new_pnm = $_SESSION['sys']['login'] . '.' . $name . '.pnm';
+
+    $preview_pnm = get_preview_pnm();
+
+    if (!file_exists($preview_pnm))
+       throw new Exception("No preview available");
+
+    copy($preview_pnm, get_scandir() . $new_pnm);
+    $list[] = $name;
+
+    series_save($list);
+
+    return ['page' => count($list), 'name' => $name];
+}
+
+function series_delete_file($name)
+{
+    $dir = get_scandir();
+
+    $path = $dir . $_SESSION['sys']['login'].'.'.$name.'.pnm';
+    if (file_exists($path)) unlink($path);
+    $path = $dir . $_SESSION['sys']['login'].'.'.$name.'.png';
+    if (file_exists($path)) unlink($path);
+    $path = $dir . $_SESSION['sys']['login'].'.'.$name.'.pdf';
+    if (file_exists($path)) unlink($path);
+}
+
+function assert_peep_image($name)
+{
+    $dir = get_scandir();
+    $new_png = $_SESSION['sys']['login'] . '.' . $name . '.png';
+
+    if (file_exists($dir.$new_png)) return;
+
+    $new_pnm = $_SESSION['sys']['login'] . '.' . $name . '.pnm';
+    $cmd = sprintf("convert -geometry 250 %s %s",
+                  escapeshellarg($dir.$new_pnm),
+                  escapeshellarg($dir.$new_png));
+
+    system($cmd);
+}
+
+function assert_peep_pdf($name)
+{
+    $scanner = ScannerBase::getScanner();
+
+    $dir = get_scandir();
+    $new_pdf = $_SESSION['sys']['login'] . '.' . $name . '.pdf';
+
+    if (file_exists($dir.$new_pdf)) return;
+
+    $new_pnm = $_SESSION['sys']['login'] . '.' . $name . '.pnm';
+    $scanner->convertPNMtoPDF($dir.$new_pnm, $dir.$new_pdf);
+}
+
+function cb_scanner()
+{
+    if (!is_dir(get_scandir()))
+       mkdir(get_scandir(), 0755, true);
+
+    if (!is_writable(get_scandir()))
+       ajax_error('documents/scanner nicht beschreibbar');
+
+    $preview = file_exists(Hallinta::instance()->basedir() . get_preview_png()) ? true : false;
+
+    return ['html' => Template::render('scanner', []), 'preview' => $preview];
+}
+
+function cb_preview()
+{
+    $scanner = ScannerBase::getScanner();
+
+    $preview_pnm = get_preview_pnm();
+    $preview_png = get_preview_png();
+
+    $scanner->scanIntoPNM($preview_pnm);
+    $scanner->convertPNMtoPNG($preview_pnm, Hallinta::instance()->basedir() . $preview_png);
+
+    return ['preview' => Hallinta::instance()->urlbase() . $preview_png];
 }
 
-?>
+function cb_rotate()
+{
+    $scanner = ScannerBase::getScanner();
+
+    $preview_pnm = get_preview_pnm();
+    $preview_png = get_preview_png();
+
+    $scanner->rotatePNM($preview_pnm, $_POST['direction']);
+    $scanner->convertPNMtoPNG($preview_pnm, Hallinta::instance()->basedir() . $preview_png);
+
+    return ['preview' => Hallinta::instance()->urlbase() . $preview_png];
+}
+
+function cb_download()
+{
+    return ['path' => pnm2pdf()];
+}
+
+function cb_store()
+{
+    return series_append();
+}
+
+function cb_peep()
+{
+    $name = $_POST['name'];
+    assert_peep_image($name);
+    assert_peep_pdf($name);
+}
+
+function cb_tidy()
+{
+    $dir = get_scandir();
+
+    foreach (new DirectoryIterator($dir) as $fileInfo) {
+       if ($fileInfo->isDot()) continue;
+       if ($fileInfo->isDir()) continue;
+
+       if (strpos($fileInfo->getFilename(), $_SESSION['sys']['login'].'.') === 0)
+           unlink($dir.$fileInfo->getFilename());
+    }
+}
+
+function cb_series_tidy()
+{
+    $dir = get_scandir();
+
+    foreach (series_load() as $name)
+       series_delete_file($name);
+
+    $path = $dir . $_SESSION['sys']['login'].'.series.pdf';
+    if (file_exists($path)) unlink($path);
+
+    $path = $dir . $_SESSION['sys']['login'].'.series';
+    if (file_exists($path)) unlink($path);
+}
+
+function cb_arrange()
+{
+    $hallinta = Hallinta::instance();
+
+    if (!is_dir(get_scandir()))
+       mkdir(get_scandir(), 0755, true);
+
+    if (!is_writable(get_scandir()))
+       ajax_error('documents/scanner nicht beschreibbar');
+
+    $page = 0;
+    $list = [];
+    foreach (series_load() as $name)
+       $list[] = ['name' => $name,
+                  'page' => ++$page,
+                  'up' => $hallinta->urlbase().'masks/'.basename(__DIR__).'/icon/up-icon.png',
+                  'down' => $hallinta->urlbase().'masks/'.basename(__DIR__).'/icon/down-icon.png',
+                  'delete' => $hallinta->urlbase().'masks/'.basename(__DIR__).'/icon/trash-icon.png',
+                  'src' => sprintf("%sajax/ajax.php?func=file&name=preview&source=%s__%s&id=%s",
+                                   $hallinta->urlbase(),
+                                   $hallinta->module(),
+                                   $hallinta->page(),
+                                   $name)];
+    if (count($list)) {
+       $list[0]['up'] = false;
+       $list[count($list)-1]['down'] = false;
+    }
+
+    return ['html' => Template::render('arrange', ['list' => $list])];
+}
+
+function cb_series()
+{
+    $dir = get_scandir();
+    $scanner = ScannerBase::getScanner();
+    $files = [];
+
+    foreach (series_load() as $name) {
+       assert_peep_pdf($name);
+       $files[] = $dir . $_SESSION['sys']['login'] . '.' . $name . '.pdf';
+    }
+
+    $outfile = $dir . $_SESSION['sys']['login'] . '.series.pdf';
+
+    $scanner->mergePDF($files, $outfile);
+}
+
+function cb_arrange_del()
+{
+    $list = series_load();
+
+    for ($i=0; $i < count($list); $i++)
+       if ($list[$i] === $_POST['name']) {
+           series_delete_file($list[$i]);
+           unset($list[$i]);
+       }
+    series_save($list);
+}
+
+function cb_arrange_up()
+{
+    $list = series_load();
+
+    for ($i=1; $i < count($list); $i++)
+       if ($list[$i] === $_POST['name']) {
+           $tmp = $list[$i];
+           $list[$i] = $list[$i-1];
+           $list[$i-1] = $tmp;
+       }
+    series_save($list);
+}
+
+function cb_arrange_down()
+{
+    $list = series_load();
+
+    for ($i=0; $i < count($list)-1; $i++)
+       if ($list[$i] === $_POST['name']) {
+           $tmp = $list[$i];
+           $list[$i] = $list[$i+1];
+           $list[$i+1] = $tmp;
+       }
+    series_save($list);
+}
+
+function use_from_scanner($field, $info)
+{
+    if (isset($_POST['path_source']) && $_POST['path_source'] == 'scanner') {
+       $preview_pdf = pnm2pdf();
+       $title = str_replace(["'", "&", ":", "/", "*", "\\"],
+                            ['', '', '', '', '', ''],
+                            $_POST['title']);
+       list($base, $fname) = unique_pathname($info['path'], $title.'.pdf');
+
+       if (copy(Hallinta::instance()->basedir() . $preview_pdf, $base.$fname) === false)
+           ajax_error("Datei kann nicht kopiert werden");
+
+       return $fname;
+    } elseif (isset($_POST['path_source']) && $_POST['path_source'] == 'series') {
+       cb_series();
+       $preview_pdf = 'archive/documents/scanner/' . $_SESSION['sys']['login'] . '.series.pdf';
+       $title = str_replace(["'", "&", ":", "/", "*", "\\"],
+                            ['', '', '', '', '', ''],
+                            $_POST['title']);
+       list($base, $fname) = unique_pathname($info['path'], $title.'.pdf');
+
+       if (copy(Hallinta::instance()->basedir() . $preview_pdf, $base.$fname) === false)
+           ajax_error("Datei kann nicht kopiert werden");
+
+       return $fname;
+    } else {
+       return NULL;
+    }
+
+    ajax_error("Something went wrong");
+}
diff --git a/masks/documents/icon/down-icon.png b/masks/documents/icon/down-icon.png
new file mode 100644 (file)
index 0000000..ee16033
Binary files /dev/null and b/masks/documents/icon/down-icon.png differ
diff --git a/masks/documents/icon/scanner.png b/masks/documents/icon/scanner.png
new file mode 100644 (file)
index 0000000..fb924fe
Binary files /dev/null and b/masks/documents/icon/scanner.png differ
diff --git a/masks/documents/icon/trash-icon.png b/masks/documents/icon/trash-icon.png
new file mode 100644 (file)
index 0000000..5b032fd
Binary files /dev/null and b/masks/documents/icon/trash-icon.png differ
diff --git a/masks/documents/icon/up-icon.png b/masks/documents/icon/up-icon.png
new file mode 100644 (file)
index 0000000..d62d2e2
Binary files /dev/null and b/masks/documents/icon/up-icon.png differ
diff --git a/masks/documents/templates/arrange.phtml b/masks/documents/templates/arrange.phtml
new file mode 100644 (file)
index 0000000..e8a4aa2
--- /dev/null
@@ -0,0 +1,28 @@
+<div>
+<div id="controls" style="margin-bottom:3px;">
+     <button id="btn_scanner_arrange_download" class="builtin" onclick="arrangement_download()" title="PDF-Datei herunterladen">Download</button>
+     <button id="btn_scanner_arrange_save" class="builtin" onclick="arrangement_save()" title="PDF-Datei archivieren">Archivieren</button>
+     <button id="btn_scanner_arrange_tidy" class="builtin" onclick="arrangement_tidy()" title="Alle Seiten löschen / Neu anfangen">Löschen</button>
+</div>
+<div id="spreview" style="min-width:400px;min-height:300px;overflow:auto;">
+<?php foreach ($list as $item) { ?>
+<div class="page" style="display:none;" data-name="<?php echo $item['name'] ?>">
+    <div style="margin-right:40px;float:left">
+    <img class="peep" src="<?php echo $item['src'] ?>" title="Seite <?php echo $item['page'].$item['name'] ?>" />
+    </div>
+    <div style="width:40px;float:left">
+      <div class="hide-on-mobile" style="padding-top:2ex;padding-bottom:3ex;">Seite <?php echo $item['page'] ?></div>
+      <?php if ($item['up']): ?>
+        <img src="<?php echo $item['up'] ?>" onclick="arrangement_up(this)" title="Nach oben" />
+      <?php endif; ?>
+      <?php if ($item['delete']): ?>
+        <img src="<?php echo $item['delete'] ?>" onclick="arrangement_del(this)" title="Löschen" />
+      <?php endif; ?>
+      <?php if ($item['down']): ?>
+        <img src="<?php echo $item['down'] ?>" onclick="arrangement_down(this)" title="Nach unten" />
+      <?php endif; ?>
+    </div>
+</div>
+<?php } ?>
+</div>
+</div>
diff --git a/masks/documents/templates/popup/close.phtml b/masks/documents/templates/popup/close.phtml
new file mode 100644 (file)
index 0000000..d0ef6d3
--- /dev/null
@@ -0,0 +1,10 @@
+<div>
+<div style="margin-top:1ex;margin-bottom:3ex;font-weight:bold;">
+Eingescannte Dateien löschen?
+</div>
+<div style="text-align:center;margin-bottom:1ex;">
+<button id="btn_scanner_close_tidy" class="builtin" onclick="scanner_tidy()" title="Dateien löschen">Löschen</button>
+&nbsp;
+<button id="btn_scanner_close_close" class="builtin" onclick="files_scanner_close_popup.closePopup()" title="Nur Schließen">Behalten</button>
+</div>
+</div>
diff --git a/masks/documents/templates/popup/filename.phtml b/masks/documents/templates/popup/filename.phtml
new file mode 100644 (file)
index 0000000..67c0d34
--- /dev/null
@@ -0,0 +1,10 @@
+<div id="popup_form_name">
+<form>
+<label for="form_name">Dateiname (ohne Suffix)</label><br>
+<input id="form_name" name="name" size="20">
+<br>
+<div class="buttons" style="margin-top: 15px;">
+<button class="custom" onclick="return build_zip(this);">Datei erstellen</button>
+</div>
+</form>
+</div>
diff --git a/masks/documents/templates/scanner.phtml b/masks/documents/templates/scanner.phtml
new file mode 100644 (file)
index 0000000..b8a1f67
--- /dev/null
@@ -0,0 +1,14 @@
+<div>
+<div id="controls" style="margin-bottom:3px;">
+     <button id="btn_scanner_preview" class="builtin" onclick="scanner_preview()" title="Neues Dokument scannen">Scan</button>
+     <button id="btn_scanner_rotate" class="builtin" onclick="scanner_rotate(<?php echo ScannerBase::RIGHT?>)" title="Dokument um 90° drehen">Drehen</button>
+     <button id="btn_scanner_rotate" class="builtin" onclick="scanner_rotate(<?php echo ScannerBase::FLIP?>)" title="Dokument um 180° drehen">Spiegeln</button>
+     <button id="btn_scanner_download" class="builtin" onclick="scanner_download()" title="PDF-Datei herunterladen">Download</button>
+     <button id="btn_scanner_download" class="builtin" onclick="scanner_store()" title="PDF-Datei zur Serie hinzufügen">Parken</button>
+     <button id="btn_scanner_save" class="builtin" onclick="scanner_save()" title="PDF-Datei archivieren">Archivieren</button>
+     <button id="btn_scanner_arrange" class="builtin" onclick="open_arrange()" title="Seiten arrangieren">Mehrseitig</button>
+</div>
+<div id="preview" style="min-width:400px;min-height:300px;overview:auto;">
+<img id="preview_img" style="display:none;width:90%;height:90%;" />
+</div>
+</div>
\ No newline at end of file
diff --git a/masks/hardware/class/hw_component.class.php b/masks/hardware/class/hw_component.class.php
new file mode 100644 (file)
index 0000000..d595ca5
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+
+class HW_Component extends DatabaseTable {
+    public function __construct($id=false)
+    {
+       parent::__construct('hw_component', $id);
+    }
+
+    public function ajaxGetUsage(Array $data)
+    {
+       $sql = <<<EOC
+SELECT hw_organisation.name AS owner,
+    to_char(starttime,'DD.MM.YYYY') AS starttime,
+    to_char(endtime,'DD.MM.YYYY') AS endtime,
+    hw_component.comment,status,hw_status.name AS status
+FROM hw_component
+JOIN hw_organisation ON owner = hw_organisation.id
+JOIN hw_status ON status = hw_status.id
+WHERE hw_component.id = %d
+EOC;
+       $sql = sprintf($sql, $this->id);
+       $info = $this->db->fetchAssoc($sql);
+
+       if (!$info)
+           throw new Exception("Cannot determine usage");
+
+       $sql = <<<EOC
+SELECT hw_compound.name,
+    to_char(hw_screw.starttime,'DD.MM.YYYY') AS starttime,
+    to_char(hw_screw.endtime,'DD.MM.YYYY') AS endtime,
+    usage
+FROM hw_screw
+JOIN hw_compound ON compound = hw_compound.id
+WHERE component = %d ORDER BY hw_screw.starttime
+EOC;
+       $sql = sprintf($sql, $this->id);
+       $screws = $this->db->fetchAssocList($sql);
+
+       $html = Template::render("usage", ['info' => $info, 'screws' => $screws]);
+
+       return ['html' => $html];
+    }
+
+    public function ajaxConnect(Array $data)
+    {
+       if (!$this->id)
+           throw new Exception("No componend selected");
+
+       if (empty($data['compound']) || empty($data['starttime']))
+           throw new Exception("Insufficient data transmitted");
+
+       $starttime = format_date($data['starttime']);
+
+       $sql = sprintf("SELECT id,compound,endtime FROM hw_screw WHERE component = %d AND starttime < '%s'::date ORDER BY starttime DESC LIMIT 1",
+                      $this->id, $starttime);
+
+       $row = $this->db->fetchAssoc($sql);
+
+       if (empty($row['endtime'])) {
+           $sql = sprintf("UPDATE hw_screw SET endtime='%s',sys_edit=now(),sys_user=%s WHERE id = %d",
+                          $starttime,
+                          $this->db->quote($_SESSION['sys']['login']), $row['id']);
+           $this->db->execute($sql);
+       }
+
+       $usage = 'NULL';
+       if (!empty($data['usage']))
+           $usage = $this->db->quote($data['usage']);
+
+       $endtime = 'NULL';
+       if (!empty($data['endtime']))
+           $endtime = "'" . format_date($data['endtime']) . "'";
+
+       $sql = sprintf("INSERT INTO hw_screw (component,compound,usage,starttime,endtime,sys_user,sys_edit) " .
+                      "VALUES (%d,%d,%s,'%s',%s,%s,now())",
+                      $this->id, $data['compound'],
+                      $usage,
+                      $starttime, $endtime,
+                      $this->db->quote($_SESSION['sys']['login']));
+       return $this->db->execute($sql);
+    }
+
+    public function ajaxDisconnect(Array $data)
+    {
+       if (!$this->id)
+           throw new Exception("No componend selected");
+
+       if (empty($data['termtime']))
+           throw new Exception("Insufficient data transmitted");
+
+       $termtime = format_date($data['termtime']);
+
+       $sql = sprintf("SELECT id,endtime FROM hw_screw WHERE component = %d AND starttime < '%s'::date ORDER BY starttime DESC LIMIT 1",
+                      $this->id, $termtime);
+
+       $row = $this->db->fetchAssoc($sql);
+
+       if (empty($row['endtime'])) {
+           $sql = sprintf("UPDATE hw_screw SET endtime='%s',sys_edit=now(),sys_user=%s WHERE id = %d",
+                          $termtime,
+                          $this->db->quote($_SESSION['sys']['login']), $row['id']);
+           $this->db->execute($sql);
+       }
+
+       if (!empty($data['comment'])) {
+           $sql = sprintf("SELECT comment FROM hw_component WHERE id = %d", $this->id);
+
+           $row = $this->db->fetchAssoc($sql);
+
+           if (!empty($row['comment']))
+               $data['comment'] = $row['comment'] . "\n\n" . $data['comment'];
+
+           $sql = sprintf("UPDATE hw_component SET comment=%s WHERE id = %d",
+                          $this->db->quote($data['comment']),
+                          $this->id);
+           $this->db->execute($sql);
+       }
+
+       $sql = sprintf("UPDATE hw_component SET endtime='%s',status=%d,sys_edit=now(),sys_user=%s WHERE id = %d",
+                      $termtime, STATUS_DEFUNCT,
+                      $this->db->quote($_SESSION['sys']['login']),
+                      $this->id);
+       return $this->db->execute($sql);
+    }
+}
index 2d04ffd..3e69517 100644 (file)
 
 define('STATUS_DEFUNCT', 3);
 
-$style[] = <<<EOC
+Styles::instance()->add("
 div#form_screw, div#form_decommission {
   padding-left: 10px;
   padding-right: 10px;
   font-size: 90%;
   color: #555;
-}
-EOC;
+}");
 
-$form = '
-<div id="form_screw">
-<form>
-<input id="form_component" name="component" type="hidden" />
-<label for="form_compound">Verbund</label><br>
-<select id="form_compound" name="compound">
-<option value="">...</option>
-</select>
-<br>
-<label for="form_starttime">Von</label><br>
-<input id="form_starttime" name="starttime" size="8">&nbsp;<img class="calendar" src="images/icons/calendar.gif" onclick="calendar(\\\'form_starttime\\\',event)" />
-<br>
-<label for="form_endtime">Bis</label><br>
-<input id="form_endtime" name="endtime" size="8">&nbsp;<img class="calendar" src="images/icons/calendar.gif" onclick="calendar(\\\'form_endtime\\\',event)" />
-<br>
-<label for="form_usage">Verwendung</label><br>
-<input id="form_usage" name="usage" size="23">
-<div class="buttons" style="margin-top: 15px;">
-<button onclick="return screw_add(this);">Verbinden</button>
-</div>
-</form>
-</div>
-<div id="form_decommission">
-<form>
-<input id="form_component2" name="component" type="hidden" />
-<label for="form_termtime">Datum</label><br>
-<input id="form_termtime" name="termtime" size="8">&nbsp;
-<img class="calendar" src="images/icons/calendar.gif" onclick="calendar(\\\'form_termtime\\\',event)" />
-<br><label for="form_termcomment">Bemerkung</label><br>
-<textarea id="form_termcomment" name="comment" cols="26" rows="5">
-</textarea>
-<div class="buttons" style="margin-top: 15px;">
-<button onclick="return decommission_component(this);">Stillegen</button>
-</div>
-</form>
-</div>
-';
-
-$javascript = <<<EOC
-var screw_popup = false;
-function screw_popup_open(visibleId, hiddenId)
-{
-    if (!screw_popup) {
-       var options = { hideOnClick: false, canDragFunc: true };
-       var content = 'CONTENT_FORM';
-
-       screw_popup = new Rico.Popup(options, false, false);
-       screw_popup.createWindow('<strong>Verbinden</strong>',content,'auto','200px');
-    }
-
-    var name = document.getElementById('edit_name');
-    if (name.value.length)
-       screw_popup.titleDiv.childNodes[0].innerHTML = name.value;
-    else
-       screw_popup.titleDiv.childNodes[0].innerHTML = 'Verbinden';
-
-    var form_id = document.getElementById('edit_id');
-    var component = document.getElementById('form_component');
-    var component2 = document.getElementById('form_component2');
-    component.value = form_id.value;
-    component2.value = form_id.value;
-
-    var formDiv = document.getElementById(visibleId);
-    formDiv.style.display = '';
-    formDiv = document.getElementById(hiddenId);
-    formDiv.style.display = 'none';
-
-    screw_popup.openPopup(100,300);
-}
-
-function screw_this()
-{
-    var eid = document.getElementById('edit_id');
-    if (!eid || !eid.value.length) return false;
-
-    screw_popup_open('form_screw','form_decommission');
-    return false;
-}
-
-function screw_callback(data)
-{
-    info('Verbindung gespeichert');
-    grid_update(second);
-}
-
-function screw_add(obj)
-{
-    screw_popup.closePopup();
-
-    var compound = document.getElementById('form_compound');
-    if (!compound.options[compound.selectedIndex].value.length) {
-       error('Kein Verbund ausgwählt');
-       return false;
-    }
-
-    var starttime = document.getElementById('form_starttime');
-    if (!starttime.value.length) {
-       error('Keine Anfangszeit angegeben');
-       return false;
-    }
-
-    var source = document.getElementById('source');
-    if (!source) return false;
-
-    var parms = 'source=' + source.innerHTML + '&callback=screw&';
-    ajax_request('function', parms+Form.serialize(obj.form), screw_callback);
-
-    starttime.value = '';
-
-    return false;
-}
-
-function decommission_callback(data)
-{
-    var name = document.getElementById('edit_name');
-    if (name.value.length)
-        info(name.value + ' stillgelegt');
-
-    grid_update(grid);
-    grid_update(second);
-}
-
-function decommission_component(obj)
-{
-    screw_popup.closePopup();
-
-    var termtime = document.getElementById('form_termtime');
-    if (!termtime.value.length) {
-       error('Keine Endzeit angegeben');
-       return false;
-    }
-
-    var source = document.getElementById('source');
-    if (!source) return false;
-
-    var parms = 'source=' + source.innerHTML + '&callback=decommission&';
-    ajax_request('function', parms+Form.serialize(obj.form), decommission_callback);
-
-    termtime.value = '';
-
-    return false;
-}
-
-function decommission()
-{
-    var eid = document.getElementById('edit_id');
-    if (!eid || !eid.value.length) return false;
-
-    screw_popup_open('form_decommission','form_screw');
-    return false;
-}
-
-EOC;
-
-$options = '<option value="">...</option>';
-if ($_GET['mask'] == 'hardware__component') {
-    $compounds = query_db("SELECT id,name FROM hw_compound ORDER BY name");
-    foreach ($compounds as $row) {
-       $options .= sprintf('<option value="%d">%s</option>', $row['id'], $row['name']);
-    }
-}
-
-$jscode[] = str_replace('CONTENT_FORM', str_replace(array('<option value="">...</option>',"\n"),
-                                                   array($options,"\\\n"), $form), $javascript);
-
-$buttons = <<<EOC
-<p style="margin-top: 0px; margin-bottom: 4px; text-align: center;">
-<button onclick="return screw_this()">Verbinden</button>
-&nbsp;
-<button onclick="return decommission()">Stillegen</button>
-</p>
-EOC;
+JavaScript::instance()->file('hardware.js');
+JavaScript::instance()->add("Hallinta.pageInit = component_init;");
 
 $mask = array(
              'table' => 'hw_component',
@@ -194,15 +23,17 @@ $mask = array(
                                            ),
                              'hwtype' => array(
                                            'name' => 'Typ',
+                                           'sqltype' => 'int',
                                            'width' => 90,
-                                           'specs' => "ClassName: 'aligncenter', filterUI: 's'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('hw_types','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT hwtype,hw_types.name FROM hw_component JOIN hw_types ON hwtype = hw_types.id ORDER BY name",
                                            ),
                              'name' => array(
                                            'name' => 'Name',
                                            'width' => 230,
-                                           'specs' => "filterUI: 't'",
+                                           'filter' => 't',
                                            ),
                              'serno' => array(
                                            'name' => 'Serial',
@@ -212,27 +43,34 @@ $mask = array(
                              'price' => array(
                                            'name' => 'Preis',
                                            'width' => 50,
-                                           'specs' => "decPlaces: 2, ClassName: 'alignrightpad', canSort: false",
+                                           'specs' => array('decPlaces' => 2, 'ClassName' => 'alignrightpad', 'canSort' => false),
                                            ),
                              'owner' => array(
                                            'name' => 'Eigner',
+                                           'sqltype' => 'int',
                                            'width' => 55,
-                                           'specs' => "ClassName: 'aligncenter', filterUI: 's'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('hw_organisation','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT owner,hw_organisation.name FROM hw_component JOIN hw_organisation ON owner = hw_organisation.id ORDER BY name",
                                            ),
                              'starttime' => array(
                                            'name' => 'Start',
                                            'width' => 75,
+                                           'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
                                            ),
                              'endtime' => array(
                                            'name' => 'End',
                                            'width' => 75,
+                                           'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
                                            ),
                              'status' => array(
                                            'name' => 'Status',
+                                           'sqltype' => 'int',
                                            'width' => 50,
-                                           'specs' => "filterUI: 's'",
+                                           'filter' => 's',
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('hw_status','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT status,hw_status.name FROM hw_component JOIN hw_status ON owner = hw_status.id ORDER BY name",
                                            ),
@@ -267,6 +105,7 @@ $mask = array(
                                            'name' => 'Von',
                                            'type' => 'date',
                                            'size' => 8,
+                                           'null' => true,
                                            ),
                              'endtime' => array(
                                            'name' => 'Bis',
@@ -285,17 +124,15 @@ $mask = array(
                                            ),
                              'screw' => array(
                                            'type' => 'html',
-                                           'code' => $buttons,
+                                           'code' => Template::render('screw/buttons', []),
                                            'sql' => false,
                                            ),
                              ),
-             'callbacks' => array(
-                                  'screw' => cb_screw,
-                                  'decommission' => cb_decommission,
-                               ),
              'second' => array(
+                 'screw' => array(
                        'title' => 'Verwendung',
                        'table' => 'hw_screw',
+                       'table_edit' => 'hw_screw',
                        'rows' => 10,
                        'join' => array(
                                        'hw_compound ON compound = hw_compound.id',
@@ -305,6 +142,7 @@ $mask = array(
                              'id' => array(
                                            'name' => 'ID',
                                            'visible' => false,
+                                           'edit' => array('Writeable' => false, 'EntryType' => 'H', 'Length' => 4, 'isKey' => true),
                                            'sql' => 'hw_screw.id',
                                            ),
                              'compound' => array(
@@ -315,120 +153,25 @@ $mask = array(
                              'starttime' => array(
                                            'name' => 'Von',
                                            'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
                                            'width' => 80,
+                                           'edit' => array('EntryType' => 'D', 'isNullable' => true, 'Writeable' => true),
                                            'sql' => 'hw_screw.starttime'
                                            ),
                              'endtime' => array(
                                            'name' => 'Bis',
                                            'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
                                            'width' => 80,
+                                           'edit' => array('EntryType' => 'D', 'isNullable' => true, 'Writeable' => true),
                                            'sql' => 'hw_screw.endtime'
                                            ),
                              'usage' => array(
                                            'name' => 'Verwendung',
                                            'width' => 270,
+                                           'edit' => array('EntryType' => 'T', 'isNullable' => false, 'Writeable' => true),
                                            ),
                                   ),
                              ),
+                     ),
              );
-
-function cb_screw()
-{
-  global $db;
-
-  if (empty($_POST['component']) || empty($_POST['compound']) || empty($_POST['starttime']))
-    return array('error' => 'Insufficient data transmitted');
-
-  $starttime = format_date($_POST['starttime']);
-
-  $sql = sprintf("SELECT id,compound,endtime FROM hw_screw WHERE component = %d AND starttime < '%s'::date ORDER BY starttime DESC LIMIT 1",
-                $_POST['component'], $starttime);
-
-  $sth = $db->query($sql);
-
-  $row = false;
-  if ($sth !== false)
-    $row = $sth->fetch();
-
-  if ($row !== false) {
-    if (empty($row['endtime'])) {
-      $sql = sprintf("UPDATE hw_screw SET endtime='%s',sys_edit=now(),sys_user=%s WHERE id = %d",
-                    $starttime,
-                    $db->quote($_SESSION['sys']['login']), $row['id']);
-      $db->query($sql);
-    }
-  }
-
-  $usage = 'NULL';
-  if (!empty($_POST['usage']))
-    $usage = $db->quote($_POST['usage']);
-
-  $endtime = 'NULL';
-  if (!empty($_POST['endtime']))
-    $endtime = "'" . format_date($_POST['endtime']) . "'";
-
-  $sql = sprintf("INSERT INTO hw_screw (component,compound,usage,starttime,endtime,sys_user,sys_edit) " .
-                "VALUES (%d,%d,%s,'%s',%s,%s,now())",
-                $_POST['component'], $_POST['compound'],
-                $usage,
-                $starttime, $endtime,
-                $db->quote($_SESSION['sys']['login']));
-  $db->query($sql);
-
-  return true;
-}
-
-function cb_decommission()
-{
-  global $db;
-
-  if (empty($_POST['component']) || empty($_POST['termtime']))
-    return array('error' => 'Insufficient data transmitted');
-
-  $termtime = format_date($_POST['termtime']);
-
-  $sql = sprintf("SELECT id,endtime FROM hw_screw WHERE component = %d AND starttime < '%s'::date ORDER BY starttime DESC LIMIT 1",
-                $_POST['component'], $termtime);
-
-  $sth = $db->query($sql);
-
-  $row = false;
-  if ($sth !== false)
-    $row = $sth->fetch();
-
-  if ($row !== false && empty($row['endtime'])) {
-    $sql = sprintf("UPDATE hw_screw SET endtime='%s',sys_edit=now(),sys_user=%s WHERE id = %d",
-                  $termtime,
-                  $db->quote($_SESSION['sys']['login']), $row['id']);
-    $db->query($sql);
-
-  }
-
-  if (!empty($_POST['comment'])) {
-    $sql = sprintf("SELECT comment FROM hw_component WHERE id = %d", $_POST['component']);
-
-    $sth = $db->query($sql);
-
-    if ($sth !== false) {
-      $row = $sth->fetch();
-      if (!empty($row['comment']))
-       $_POST['comment'] = $row['comment'] . "\n\n" . $_POST['comment'];
-    }
-
-    $sql = sprintf("UPDATE hw_component SET comment=%s WHERE id = %d",
-                  $db->quote($_POST['comment']),
-                  $_POST['component']);
-    $db->query($sql);
-  }
-
-
-  $sql = sprintf("UPDATE hw_component SET endtime='%s',status=%d,sys_edit=now(),sys_user=%s WHERE id = %d",
-                $termtime, STATUS_DEFUNCT,
-                $db->quote($_SESSION['sys']['login']),
-                $_POST['component']);
-  $db->query($sql);
-
-  return true;
-}
-
-?>
index ab7fcd1..75f65c8 100644 (file)
@@ -1,56 +1,12 @@
 <?php
 
-$jscode[] = <<<EOC
-var component_popup = false;
+JavaScript::instance()->file('hardware.js');
 
-function component_callback(data)
-{
-    component_popup.contentDiv.innerHTML = data['info'] + data['screws'];
-}
-
-function component_popup_open(e, component)
-{
-    if (!component_popup) {
-       var options = { hideOnClick: false, canDragFunc: true };
-
-       component_popup = new Rico.Popup(options, false, false);
-       component_popup.createWindow('<strong>Verwendung</strong>','','auto','280px');
-    }
-
-    var source = document.getElementById('source');
-    if (!source) return false;
-
-    var parms = 'source=' + source.innerHTML + '&callback=component&component=' + component;
-    ajax_request('function', parms, component_callback);
-
-    if (component_popup.divPopup.style.display == 'none')
-       component_popup.openPopup(e.clientX-50, e.clientY-150);
-}
-
-
-function drill_second(e)
-{
-    if (e.originalTarget && e.originalTarget.target && e.originalTarget.target == '_top')
-       return;
-
-    var id = 0; // Column 0 contains ID
-    var row = second.edit.drillDown(e,0,0);
-    var cell = second.columns[id].cell(row);
-    if (!cell) return;
-    var value = cell.innerHTML;
-
-    if (!value.length || value == '&nbsp;') return;
-
-    component_popup_open(e, value);
-};
-EOC;
-
-$style[] = <<<EOC
+Styles::instance()->add("
 table.tinfo {
     font-size: 10px;
     color: #555;
-}
-EOC;
+}");
 
 $mask = array(
              'table' => 'hw_compound',
@@ -75,7 +31,8 @@ $mask = array(
                                            ),
                              'location' => array(
                                            'name' => 'Location',
-                                           'specs' => "filterUI: 's',ClassName: 'aligncenter'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('hw_organisation','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT location,hw_organisation.name FROM hw_compound JOIN hw_organisation ON location = hw_organisation.id ORDER BY name",
                                            'width' => 100,
@@ -83,15 +40,13 @@ $mask = array(
                              'status' => array(
                                            'name' => 'Status',
                                            'width' => 50,
-                                           'specs' => "filterUI: 's'",
+                                           'filter' => 's',
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('hw_status','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT status,hw_status.name FROM hw_compound JOIN hw_status ON status = hw_status.id ORDER BY name",
                                            ),
                              ),
-             'callbacks' => array(
-                                  'component' => cb_component,
-                               ),
              'second' => array(
+                  'components' => array(
                        'title' => 'Komponenten',
                        'rows' => 8,
                        'table' => 'hw_screw',
@@ -99,7 +54,7 @@ $mask = array(
                                        'hw_component ON component = hw_component.id',
                                        ),
                        'where' => 'compound = {id} AND hw_screw.endtime IS NULL ORDER BY name',
-                       'onclick' => 'drill_second',
+                       'onclick' => 'component_drilldown',
                        'list' => array(
                              'component' => array(
                                            'name' => 'ID',
@@ -112,13 +67,14 @@ $mask = array(
                              'starttime' => array(
                                            'name' => 'Seit',
                                            'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
                                            'width' => 80,
                                            'sql' => 'hw_screw.starttime'
                                            ),
                              'price' => array(
                                            'name' => 'Preis',
                                            'width' => 50,
-                                           'specs' => "decPlaces: 2, ClassName: 'alignrightpad', canSort: false",
+                                           'specs' => array('decPlaces' => 2, 'ClassName' => 'alignrightpad', 'canSort' => false),
                                            ),
                              'usage' => array(
                                            'name' => 'Verwendung',
@@ -126,6 +82,7 @@ $mask = array(
                                            ),
                                   ),
                                ),
+                            ),
              'edit' => array(
                              'name' => array(
                                            'name' => 'Name',
@@ -163,66 +120,3 @@ $mask = array(
                                            ),
                              ),
              );
-
-function cb_component()
-{
-  global $db;
-
-  $data = array('info' => '', 'screws' => '');
-  $sql = <<<EOC
-SELECT hw_organisation.name AS owner,
-    to_char(starttime,'DD.MM.YYYY') AS starttime,
-    to_char(endtime,'DD.MM.YYYY') AS endtime,
-    hw_component.comment,status,hw_status.name AS status
-FROM hw_component
-JOIN hw_organisation ON owner = hw_organisation.id
-JOIN hw_status ON status = hw_status.id
-WHERE hw_component.id = %d
-EOC;
-  $sql = sprintf($sql, $_POST['component']);
-  $sth = $db->query($sql);
-
-  $row = false;
-  if ($sth !== false)
-    $row = $sth->fetch();
-
-  if ($row !== false) {
-    $info = '<table border="0" cellpadding="0" class="tinfo">';
-    $info .= sprintf('<tr><td>Eigentümer</td><td>:<td><td>%s</td></tr>', $row['owner']);
-    $info .= sprintf('<tr><td>Von</td><td>:<td><td>%s</td></tr>', $row['starttime']);
-    if (!empty($row['endtime']))
-      $info .= sprintf('<tr><td>Bis</td><td>:<td><td>%s</td></tr>', $row['endtime']);
-    if (!empty($row['comment']))
-      $info .= sprintf('<tr><td valign="top">Bemerkung</td><td valign="top">:<td><td>%s</td></tr>', $row['comment']);
-    $info .= sprintf('<tr><td>Status</td><td>:<td><td>%s</td></tr>', $row['status']);
-    $info .= '</table>';
-    $data['info'] = $info;
-  }
-
-  $sql = <<<EOC
-SELECT hw_compound.name,
-    to_char(hw_screw.starttime,'DD.MM.YYYY') AS starttime,
-    to_char(hw_screw.endtime,'DD.MM.YYYY') AS endtime,
-    usage
-FROM hw_screw
-JOIN hw_compound ON compound = hw_compound.id
-WHERE component = %d ORDER BY hw_screw.starttime
-EOC;
-  $sql = sprintf($sql, $_POST['component']);
-  $sth = $db->query($sql);
-
-  $screws = '<table width="99%" border="0" cellspacing="0" class="tinfo">';
-  $screws .= '<tr bgcolor="#b3ceff"><th>Name</th><th>Von</th><th>Bis</th><th>Verwendung</th></tr>';
-
-  while ($row = $sth->fetch()) {
-    $screws .= sprintf('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
-                      $row['name'], $row['starttime'], $row['endtime'], $row['usage']);
-
-  }
-  $screws .= '</table>';
-  $data['screws'] = $screws;
-
-  return $data;
-}
-
-?>
diff --git a/masks/hardware/hardware.js b/masks/hardware/hardware.js
new file mode 100644 (file)
index 0000000..4e97901
--- /dev/null
@@ -0,0 +1,134 @@
+var screw_popup = false;
+var screw_form = 'CONTENT_FORM';
+var component_popup = false;
+
+function component_init()
+{
+    ajax_request('Template/Load', 'id=screw/popup', function(data){
+       screw_form = data.data;
+    });
+}
+
+function screw_popup_open(visibleId, hiddenId)
+{
+    if (!screw_popup) {
+       screw_popup = new Rico.Window('Verbinden');
+       $(screw_popup.contentDiv).html(screw_form);
+    }
+
+    var name = document.getElementById('edit_name');
+    if (name.value.length)
+       screw_popup.titleDiv.childNodes[0].innerHTML = name.value;
+    else
+       screw_popup.titleDiv.childNodes[0].innerHTML = 'Verbinden';
+
+    var form_id = document.getElementById('edit_id');
+    var component = document.getElementById('form_component');
+    var component2 = document.getElementById('form_component2');
+    component.value = form_id.value;
+    component2.value = form_id.value;
+
+    var formDiv = document.getElementById(visibleId);
+    formDiv.style.display = '';
+    formDiv = document.getElementById(hiddenId);
+    formDiv.style.display = 'none';
+
+    screw_popup.openPopup(100,300);
+}
+
+function screw_this()
+{
+    var eid = document.getElementById('edit_id');
+    if (!eid || !eid.value.length) return false;
+
+    screw_popup_open('form_screw','form_decommission');
+    return false;
+}
+
+function screw_add(obj)
+{
+    screw_popup.closePopup();
+
+    if (!$('#form_compound').val().length) {
+       error('Kein Verbund ausgwählt');
+       return false;
+    }
+
+    if (!$('#form_starttime').val().length) {
+       error('Keine Anfangszeit angegeben');
+       return false;
+    }
+
+    ajax_request('HW_Component/Connect', $(obj.form).serialize(), function(data){
+       info('Verbindung gespeichert');
+       if (typeof Hallinta.seconds.screw == 'object')
+           grid_update(Hallinta.seconds.screw.grid);
+    });
+
+    starttime.value = '';
+
+    return false;
+}
+
+function decommission_component(obj)
+{
+    screw_popup.closePopup();
+
+    if (!$('#form_termtime').val().length) {
+       error('Keine Endzeit angegeben');
+       return false;
+    }
+
+    ajax_request('HW_Component/Disconnect', $(obj.form).serialize(), function(data){
+       if ($('#edit_name').val().length)
+            info($('#edit_name').val() + ' stillgelegt');
+
+       grid_update(Hallinta.grid);
+       if (typeof Hallinta.seconds.screw == 'object')
+           grid_update(Hallinta.seconds.screw.grid);
+    });
+
+    termtime.value = '';
+
+    return false;
+}
+
+function decommission()
+{
+    if (!$('#edit_id').val().length)
+       return false;
+
+    screw_popup_open('form_decommission','form_screw');
+    return false;
+}
+
+function component_popup_open(e, component)
+{
+    ajax_request('HW_Component/GetUsage', 'id=' + component, function(data){
+       if (!component_popup) {
+           component_popup = new Rico.Window('Verwendung', {zIndex: 100});
+           component_popup.contentDiv.innerHTML = data['html'];
+           component_popup.centerPopup();
+       } else {
+           component_popup.contentDiv.innerHTML = data['html'];
+           component_popup.openPopup();
+       }
+    });
+}
+
+function component_drilldown(e)
+{
+    if (e.originalTarget && e.originalTarget.target && e.originalTarget.target == '_top')
+       return;
+
+    var id = 0; // Column 0 contains ID
+
+    var row = Hallinta.seconds.components.edit.drillDown(e,0,0);
+    var cell = Hallinta.seconds.components.grid.columns[id].cell(row);
+    if (!cell) return;
+    var value = cell.innerHTML;
+
+    if (!value.length || value == '&nbsp;') return;
+
+    component_popup_open(e, value);
+};
diff --git a/masks/hardware/templates/screw/buttons.phtml b/masks/hardware/templates/screw/buttons.phtml
new file mode 100644 (file)
index 0000000..18e0226
--- /dev/null
@@ -0,0 +1,5 @@
+<p style="margin-top: 0px; margin-bottom: 4px; text-align: center;">
+<button onclick="return screw_this()">Verbinden</button>
+&nbsp;
+<button onclick="return decommission()">Stillegen</button>
+</p>
diff --git a/masks/hardware/templates/screw/popup.phtml b/masks/hardware/templates/screw/popup.phtml
new file mode 100644 (file)
index 0000000..d8d761c
--- /dev/null
@@ -0,0 +1,40 @@
+<div id="form_screw">
+<form>
+<input id="form_component" name="id" type="hidden" />
+<label for="form_compound">Verbund</label><br>
+<select id="form_compound" name="compound">
+<option value="">...</option>
+<?php
+     foreach (Database::get()->fetchObjectList("SELECT id,name AS text FROM hw_compound ORDER BY text") as $row) {
+?>
+        <option value="<?php echo $row->id; ?>"><?php echo $row->text; ?></option>
+<?php } ?>
+</select>
+<br>
+<label for="form_starttime">Von</label><br>
+<input id="form_starttime" name="starttime" size="8">&nbsp;<img class="calendar" src="<?php echo Hallinta::instance()->urlbase(); ?>images/icons/calendar.gif" onclick="calendar(\'form_starttime\',event)" />
+<br>
+<label for="form_endtime">Bis</label><br>
+<input id="form_endtime" name="endtime" size="8">&nbsp;<img class="calendar" src="<?php echo Hallinta::instance()->urlbase(); ?>images/icons/calendar.gif" onclick="calendar(\'form_endtime\',event)" />
+<br>
+<label for="form_usage">Verwendung</label><br>
+<input id="form_usage" name="usage" size="23">
+<div class="buttons" style="margin-top: 15px;">
+<button onclick="return screw_add(this);">Verbinden</button>
+</div>
+</form>
+</div>
+<div id="form_decommission">
+<form>
+<input id="form_component2" name="id" type="hidden" />
+<label for="form_termtime">Datum</label><br>
+<input id="form_termtime" name="termtime" size="8">&nbsp;
+<img class="calendar" src="<?php echo Hallinta::instance()->urlbase(); ?>images/icons/calendar.gif" onclick="calendar('form_termtime',event)" />
+<br><label for="form_termcomment">Bemerkung</label><br>
+<textarea id="form_termcomment" name="comment" cols="26" rows="5">
+</textarea>
+<div class="buttons" style="margin-top: 15px;">
+<button onclick="return decommission_component(this);">Stillegen</button>
+</div>
+</form>
+</div>
diff --git a/masks/hardware/templates/usage.phtml b/masks/hardware/templates/usage.phtml
new file mode 100644 (file)
index 0000000..486c2f9
--- /dev/null
@@ -0,0 +1,20 @@
+<div style="min-width: 450px; max-width: 700px;">
+<table border="0" cellpadding="0" class="tinfo">
+<tr><td>Eigentümer</td><td>:<td><td><?php echo $info['owner']; ?></td></tr>
+<tr><td>Von</td><td>:<td><td><?php echo $info['starttime']; ?></td></tr>
+<?php if (!empty($info['endtime'])) { ?>
+     <tr><td>Bis</td><td>:<td><td><?php echo $info['endtime']; ?></td></tr>
+<?php } ?>
+<?php if (!empty($info['comment'])) { ?>
+    <tr><td valign="top">Bemerkung</td><td valign="top">:<td><td><?php echo nl2br($info['comment']); ?></td></tr>
+<?php } ?>
+<tr><td>Status</td><td>:<td><td><?php echo $info['status'];?></td></tr>
+</table>
+
+<table width="99%" border="0" cellspacing="0" class="tinfo">
+<tr bgcolor="#b3ceff"><th>Name</th><th>Von</th><th>Bis</th><th>Verwendung</th></tr>
+<?php foreach ($screws as $screw) { ?>
+    <tr><td><?php echo $screw['name']; ?></td><td><?php echo $screw['starttime']; ?></td><td><?php echo $screw['endtime']; ?></td><td><?php echo $screw['usage']; ?></td></tr>
+<?php } ?>
+</table>
+</div>
diff --git a/masks/system/group.php b/masks/system/group.php
new file mode 100644 (file)
index 0000000..d205453
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+JavaScript::instance()->file('lib/ricoTableColumnDB.js');
+
+$mask = array(
+             'table' => 'sys_group',
+             'title' => 'Gruppenverwaltung',
+             'list' => array(
+                             'id' => array(
+                                           'name' => 'ID',
+                                           'visible' => false,
+                                           ),
+                             'gname' => array(
+                                           'name' => 'Gruppe',
+                                           'width' => 70,
+                                           ),
+                             'name' => array(
+                                           'name' => 'Name',
+                                           'width' => 150,
+                                           ),
+                             'sys_user' => array(
+                                           'name' => 'Bearb.',
+                                           'width' => 50,
+                                           ),
+                             'sys_edit' => array(
+                                           'name' => 'Geändert',
+                                           'width' => 80,
+                                           'type' => 'date',
+                                           'specs' => array('dateFmt' => 'yyyy-mm-dd'),
+                                           ),
+                             ),
+             'second' => array(
+                 'user' => array(
+                        'title' => 'Mitglieder',
+                        'table' => 'sys_user',
+                        'rows' => 10,
+                        'width' => 410,
+                        'list' => array(
+                                        'id' => array(
+                                                      'name' => 'ID',
+                                                      'visible' => false,
+                                                      'sql' => 'sys_user.id',
+                                                      ),
+                                        'login' => array(
+                                                         'name' => 'Login',
+                                                         'width' => 70,
+                                                         ),
+                                        'name' => array(
+                                                        'name' => 'Name',
+                                                        'width' => 120,
+                                                        'sql' => 'sys_user.name',
+                                                        ),
+                                        'email' => array(
+                                                         'name' => 'E-Mail',
+                                                         'width' => 150,
+                                                         ),
+                                        'checked' => array(
+                                                           'name' => 'Perm',
+                                                           'width' => 40,
+                                                           'sql' => '(SELECT count(*)
+                                                               FROM sys_group_user
+                                                               WHERE sys_user_id = sys_user.id
+                                                                 AND sys_group_id = {id})',
+                                                           'control' => "new Rico.TableColumn.checkboxDB(0," .
+                                                                        "Hallinta.baseURL+'ajax/ricoUpdateConnection.php')",
+                                                           'filter' => 'c',
+                                                           'specs' => array('ClassName' => 'aligncenter has-checkbox', 'canSort' => true),
+                                                           'update' => array('table' => 'sys_group_user',
+                                                                             'basecol' => 'sys_group_id',
+                                                                             'refcol' => 'sys_user_id',
+                                                                             'reftable' => 'sys_user',
+                                                                             'refid' => 'sys_user.id',
+                                                                             ),
+                                                           ),
+                                        ),
+                                  ),
+                 'menuitems' => array(
+                        'title' => 'Menüpunkte',
+                        'table' => 'sys_menuitem',
+                        'rows' => 15,
+                        'width' => 500,
+                        'list' => array(
+                                        'id' => array(
+                                                      'name' => 'ID',
+                                                      'visible' => false,
+                                                      'sql' => 'sys_menuitem.id',
+                                                      ),
+                                        'parent' => array(
+                                                          'name' => 'Super',
+                                                          'sqltype' => 'int',
+                                                          'filter' => 's',
+                                                          'width' => 120,
+                                                          'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_menuitem','id','title', [0 => 'Root']).", 0, '')",
+                                                          ),
+                                        'priority' => array(
+                                                            'name' => 'Prio',
+                                                            'width' => 50,
+                                                            'type' => 'number',
+                                                            'specs' => array('ClassName' => 'aligncenter'),
+                                                            'visible' => false,
+                                                            ),
+                                        'module' => array(
+                                                          'filter' => 's',
+                                                          'name' => 'Modul',
+                                                          'width' => 90,
+                                                          ),
+                                        'page' => array(
+                                                        'filter' => 't',
+                                                        'name' => 'Page',
+                                                        'width' => 100,
+                                                        ),
+                                        'title' => array(
+                                                         'name' => 'Titel',
+                                                         'filter' => 't',
+                                                         'width' => 100,
+                                                         ),
+                                        'checked' => array(
+                                                           'name' => 'Perm',
+                                                           'width' => 40,
+                                                           'sql' => '(SELECT count(*)
+                                                               FROM sys_group_menuitem
+                                                               WHERE sys_menuitem_id = sys_menuitem.id
+                                                                 AND sys_group_id = {id})',
+                                                           'control' => "new Rico.TableColumn.checkboxDB(0," .
+                                                                        "Hallinta.baseURL+'ajax/ricoUpdateConnection.php')",
+                                                           'filter' => 'c',
+                                                           'specs' => array('ClassName' => 'aligncenter has-checkbox', 'canSort' => true),
+                                                           'update' => array('table' => 'sys_group_menuitem',
+                                                                             'basecol' => 'sys_group_id',
+                                                                             'refcol' => 'sys_menuitem_id',
+                                                                             'reftable' => 'sys_menuitem',
+                                                                             'refid' => 'sys_menuitem.id',
+                                                                             ),
+                                                           ),
+                                        ),
+                                  ),
+                               ),
+             'edit' => array(
+                             'gname' => array(
+                                           'name' => 'Gruppe',
+                                           'type' => 'text',
+                                           'size' => 20,
+                                           ),
+                             'name' => array(
+                                           'name' => 'Name',
+                                           'type' => 'text',
+                                           'size' => 20,
+                                           ),
+                             ),
+             );
+
diff --git a/masks/system/menuitem.js b/masks/system/menuitem.js
new file mode 100644 (file)
index 0000000..c721b21
--- /dev/null
@@ -0,0 +1,47 @@
+function menuitem_init()
+{
+    menuitem_update_parent();
+    menuitem_update_module();
+    $('#edit_module').change(menuitem_update_page);
+}
+
+function menuitem_update_parent()
+{
+    ajax_request('MenuItem/GetParent', '', function(data){
+       select_update('edit_parent', data.list, 1);
+    });
+}
+
+function menuitem_update_module()
+{
+    ajax_request('MenuItem/GetModules', '', function(data){
+       select_update('edit_module', data.list, 1);
+    });
+}
+
+function menuitem_update_page(page)
+{
+    var module = $('#edit_module').val();
+
+    if (!module.length) {
+       select_update('edit_page', [], 1);
+       return;
+    }
+
+    ajax_request('MenuItem/GetPages', 'module='+module, function(data){
+       select_update('edit_page', data.list, 1);
+       if (page)
+           $('#edit_page').val(page);
+    });
+}
+
+function menuitem_post_fetch(data)
+{
+    menuitem_update_page(data.page);
+}
+
+function menuitem_post_insert(data)
+{
+    if (!$('#edit_module').val().length && !$('#edit_page').val().length)
+       menuitem_update_parent();
+}
diff --git a/masks/system/menuitem.php b/masks/system/menuitem.php
new file mode 100644 (file)
index 0000000..172471f
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+JavaScript::instance()->file('lib/ricoTableColumnDB.js');
+JavaScript::instance()->file('menuitem.js');
+JavaScript::instance()->add("Hallinta.postLoadForm = menuitem_init;");
+JavaScript::instance()->add("Hallinta.postFetch = menuitem_post_fetch;");
+JavaScript::instance()->add("Hallinta.postInsert = menuitem_post_insert;");
+
+$mask = array(
+             'table' => 'sys_menuitem',
+             'title' => 'Liste der Menüpunkte',
+             'list' => array(
+                             'id' => array(
+                                           'name' => 'ID',
+                                           'visible' => false,
+                                           ),
+                             'parent' => array(
+                                           'name' => 'Super',
+                                           'sqltype' => 'int',
+                                           'filter' => 's',
+                                           'width' => 120,
+                                           'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_menuitem','id','title', [0 => 'Root']).", 0, '')",
+#                                          'distinct' => "SELECT DISTINCT abteilung,metro_abteilung.name FROM metro_artikel JOIN metro_abteilung ON abteilung = metro_abteilung.id ORDER BY name",
+                                           ),
+                             'priority' => array(
+                                           'name' => 'Prio',
+                                           'width' => 50,
+                                           'type' => 'number',
+                                           'specs' => array('ClassName' => 'aligncenter'),
+                                           ),
+                             'module' => array(
+                                           'filter' => 's',
+                                           'name' => 'Modul',
+                                           'width' => 90,
+                                           ),
+                             'page' => array(
+                                           'filter' => 't',
+                                           'name' => 'Page',
+                                           'width' => 100,
+                                           ),
+                             'title' => array(
+                                           'name' => 'Titel',
+                                           'filter' => 't',
+                                           'width' => 100,
+                                           ),
+                             'edit' => array(
+                                           'name' => 'Edit',
+                                           'width' => 40,
+                                           'specs' => array('ClassName' => 'aligncenter has-checkbox', 'canSort' => false),
+                                           'control' => "new Rico.TableColumn.checkbox(1,0,0,1)",
+                                           ),
+                             ),
+             'edit' => array(
+                             'parent' => array(
+                                           'name' => 'Super',
+                                           'type' => 'select',
+                                           'options' => [['id'=>'0', 'text'=>'Root']],
+                                           'options_string' => true,
+                                           ),
+                             'module' => array(
+                                           'name' => 'Modul',
+                                           'type' => 'select',
+                                           'options' => [['id'=>NULL, 'text'=>'']],
+                                           'options_string' => true,
+                                           'size' => 24,
+                                           'null' => true,
+                                           ),
+                             'page' => array(
+                                           'name' => 'Seite',
+                                           'type' => 'select',
+                                           'options' => [['id'=>NULL, 'text'=>'']],
+                                           'options_string' => true,
+                                           'size' => 24,
+                                           'null' => true,
+                                           ),
+                             'priority' => array(
+                                           'name' => 'Priorität',
+                                           'type' => 'number',
+                                           'size' => 10,
+                                           'required' => true,
+                                           ),
+                             'title' => array(
+                                           'name' => 'Titel',
+                                           'type' => 'text',
+                                           'size' => 24,
+                                           'required' => true,
+                                           ),
+                             'tooltip' => array(
+                                           'name' => 'Tooltip',
+                                           'type' => 'text',
+                                           'size' => 24,
+                                           ),
+                             'edit' => array(
+                                           'name' => 'Edit',
+                                           'type' => 'boolean',
+                                           ),
+                             'shadow' => array(
+                                           'name' => 'shadow',
+                                           'type' => 'boolean',
+                                           ),
+                             ),
+             'second' => array(
+                 'perms' => array(
+                        'title' => 'Freigaben',
+                        'table' => 'sys_group',
+                        'rows' => 10,
+                        'width' => 320,
+                        'list' => array(
+                                        'id' => array(
+                                                      'name' => 'ID',
+                                                      'visible' => false,
+                                                      'sql' => 'sys_group.id',
+                                                      ),
+                                        'gname' => array(
+                                                         'name' => 'Name',
+                                                         'width' => 80,
+                                                         'type' => 'text',
+                                                         ),
+                                        'name' => array(
+                                                        'name' => 'Name',
+                                                        'width' => 150,
+                                                        'type' => 'text',
+                                                        ),
+                                        'checked' => array(
+                                                           'name' => 'Perm',
+                                                           'width' => 40,
+                                                           'sql' => '(SELECT count(*)
+                                                               FROM sys_group_menuitem
+                                                               WHERE sys_group_id = sys_group.id
+                                                                 AND sys_menuitem_id = {id})',
+                                                           'control' => "new Rico.TableColumn.checkboxDB(0," .
+                                                                        "Hallinta.baseURL+'ajax/ricoUpdateConnection.php')",
+                                                           'filter' => 'c',
+                                                           'specs' => array('ClassName' => 'aligncenter', 'canSort' => true),
+                                                           'update' => array('table' => 'sys_group_menuitem',
+                                                                             'basecol' => 'sys_menuitem_id',
+                                                                             'refcol' => 'sys_group_id',
+                                                                             'reftable' => 'sys_group',
+                                                                             'refid' => 'sys_group.id',
+                                                                             ),
+                                                           ),
+                                        ),
+                                  ),
+                               ),
+             );
diff --git a/masks/system/sys_group.php b/masks/system/sys_group.php
deleted file mode 100644 (file)
index aca321f..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-$mask = array(
-             'table' => 'sys_group',
-             'title' => 'Liste der Gruppen',
-             'list' => array(
-                             'id' => array(
-                                           'name' => 'ID',
-                                           'visible' => false,
-                                           ),
-                             'gname' => array(
-                                           'name' => 'Gruppe',
-                                           'width' => 70,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'width' => 150,
-                                           ),
-                             'sys_user' => array(
-                                           'name' => 'Bearb.',
-                                           'width' => 50,
-                                           ),
-                             'sys_edit' => array(
-                                           'name' => 'Geändert',
-                                           'width' => 80,
-                                           'type' => 'date',
-                                           ),
-                             ),
-             'edit' => array(
-                             'gname' => array(
-                                           'name' => 'Gruppe',
-                                           'type' => 'text',
-                                           'size' => 20,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'type' => 'text',
-                                           'size' => 20,
-                                           ),
-                             ),
-             );
-
-?>
diff --git a/masks/system/sys_group_mask.php b/masks/system/sys_group_mask.php
deleted file mode 100644 (file)
index 9a93c55..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-define('VARNAME','system__sys_group_mask.group');
-
-$jscode[] = <<<EOC
-
-Rico.moduleDependencies['checkboxDB'] = ['+LiveGrid', '../ricoTableColumnDB.js'];
-Rico.loadModule('checkboxDB');
-
-function select_group_calback(data)
-{
-  grid_update(grid);
-}
-
-function select_group(obj)
-{
-  if (!obj.options[obj.selectedIndex].value.length)
-    val = -1;
-  else
-    val = obj.options[obj.selectedIndex].value;
-
-  ajax_request('setvar','source=system__sys_group_mask&name=group&value='+val,select_group_calback);
-
-  if (obj.options[obj.selectedIndex].value.length)
-    info('Gruppe ' + obj.options[obj.selectedIndex].innerHTML + ' ausgewählt');
-  else
-    info('');
-}
-EOC;
-
-if (empty($_SESSION[VARNAME])) $_SESSION[VARNAME] = 0;
-
-$mask = array(
-             'table' => 'sys_mask',
-             'title' => 'Berechtigungen',
-             'select' => array(
-                               'title' => 'Auswahl',
-                               'options' => 'SELECT id,name AS text FROM sys_group ORDER BY name',
-                               'default' => 'Gruppe wählen',
-                               'selected' => $_SESSION[VARNAME],
-                               'onchange' => 'select_group(this)',
-                               ),
-             'join' => array('sys_menu ON sys_mask.menu = sys_menu.id'),
-             'list' => array(
-                             'id' => array(
-                                           'name' => 'ID',
-                                           'sql' => 'sys_mask.id',
-                                           'visible' => false,
-                                           ),
-                             'menu' => array(
-                                           'name' => 'Menü',
-                                           'width' => 80,
-                                           'sql' => 'sys_menu.name',
-                                           ),
-                             'menutitle' => array(
-                                           'name' => 'Menüpunkt',
-                                           'width' => 170,
-                                           'sql' => 'sys_mask.menutitle',
-                                           ),
-                             'title' => array(
-                                           'name' => 'Beschreibung',
-                                           'width' => 150,
-                                           'sql' => 'sys_mask.title',
-                                           ),
-                             'edit' => array(
-                                           'name' => 'Edit',
-                                           'width' => 40,
-                                           'specs' => "ClassName: 'aligncenter', canSort: false",
-                                           'sql' => 'sys_mask.edit',
-                                           ),
-                             'checked' => array(
-                                           'name' => 'Menü',
-                                           'width' => 40,
-                                           'sql' => '(SELECT count(*) FROM sys_group_mask WHERE gid = '
-                                               . intval($_SESSION[VARNAME])
-                                               . ' AND mask = sys_mask.id)',
-                                           'control' => "new Rico.TableColumn.checkboxDB(0, 'ajax/ricoUpdateConnection.php')",
-                                           'specs' => "filterUI: 'c', ClassName: 'aligncenter', canSort: false",
-                                           'update' => array('table' => 'sys_group_mask',
-                                                             'basecol' => 'gid',
-                                                             'baseval' => $_SESSION[VARNAME],
-                                                             'refcol' => 'mask',
-                                                             'reftable' => 'sys_mask',
-                                                             'refid' => 'sys_mask.id',
-                                                             ),
-                                           ),
-                             ),
-             'variables' => array(
-                                  'group' => array(),
-                                  ),
-             );
-
-?>
diff --git a/masks/system/sys_mask.php b/masks/system/sys_mask.php
deleted file mode 100644 (file)
index d257b6e..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-<?php
-
-function discover_cmp($a, $b)
-{
-  return strcmp($a['id'],$b['id']);
-}
-
-function discover_masks()
-{
-  $info = array();
-
-  if (($modules = opendir($_SESSION['sys']['basedir'].'masks')) === false)
-    return $info;
-
-  while (($module = readdir($modules)) !== false)
-    if (is_dir($_SESSION['sys']['basedir'].'masks/'.$module)
-       && $module != '.' && $module != '..') {
-
-      if (($d = opendir($_SESSION['sys']['basedir'].'masks/'.$module)) === false)
-       continue;
-
-      while (($file = readdir($d)) !== false)
-       if (substr($file,-4) == '.php') {
-         $fname = substr($file,0,-4);
-         $info[] = array('id' => $module.'|'.$fname, 'text' => $module.'|'.$fname);
-       }
-      closedir($d);
-    }
-  closedir($modules);
-
-  usort($info,'discover_cmp');
-  return $info;
-}
-
-$mask = array(
-             'table' => 'sys_mask',
-             'title' => 'Liste der Menüpunkte',
-             'join' => array('sys_menu ON sys_mask.menu = sys_menu.id'),
-             'list' => array(
-                             'id' => array(
-                                           'name' => 'ID',
-                                           'visible' => false,
-                                           'sql' => 'sys_mask.id',
-                                           ),
-                             'fname' => array(
-                                           'name' => 'Dateiname',
-                                           'width' => 170,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'width' => 150,
-                                           'sql' => 'sys_mask.name',
-                                           ),
-                             'menutitle' => array(
-                                           'name' => 'Menüpunkt',
-                                           'width' => 100,
-                                           ),
-                             'menu' => array(
-                                           'name' => 'Menü',
-                                           'sql' => 'sys_menu.name',
-                                           'width' => 100,
-                                           ),
-                             'edit' => array(
-                                           'name' => 'Edit',
-                                           'width' => 40,
-                                           'specs' => "ClassName: 'aligncenter', canSort: false",
-                                           ),
-                             'priority' => array(
-                                           'name' => 'Priorität',
-                                           'width' => 60,
-                                           'type' => 'number',
-                                           'specs' => "ClassName: 'alignright', canSort: false",
-                                           'sql' => 'sys_mask.priority',
-                                           ),
-                             ),
-             'edit' => array(
-                             'fname' => array(
-                                           'name' => 'Dateiname',
-                                           'type' => 'select',
-                                           'options' => discover_masks(),
-                                           'options_string' => true,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'type' => 'text',
-                                           'size' => 24,
-                                           ),
-                             'menutitle' => array(
-                                           'name' => 'Menüpunkt',
-                                           'type' => 'text',
-                                           'size' => 24,
-                                           'required' => true,
-                                           ),
-                             'title' => array(
-                                           'name' => 'Beschreibung',
-                                           'type' => 'text',
-                                           'size' => 24,
-                                           'null' => true,
-                                           ),
-                             'menu' => array(
-                                           'name' => 'Menü',
-                                           'type' => 'select',
-                                           'options' => 'SELECT id,name AS text FROM sys_menu ORDER BY name',
-                                           ),
-                             'edit' => array(
-                                           'name' => 'Edit',
-                                           'type' => 'boolean',
-                                           ),
-                             'shadow' => array(
-                                           'name' => 'shadow',
-                                           'type' => 'boolean',
-                                           ),
-                             'priority' => array(
-                                           'name' => 'Priorität',
-                                           'type' => 'number',
-                                           'size' => 10,
-                                           ),
-                             ),
-             );
-
-?>
diff --git a/masks/system/sys_menu.php b/masks/system/sys_menu.php
deleted file mode 100644 (file)
index 879bad4..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-$mask = array(
-             'table' => 'sys_menu',
-             'title' => 'Liste der Menüs',
-             'list' => array(
-                             'id' => array(
-                                           'name' => 'ID',
-                                           'visible' => false,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'width' => 150,
-                                           ),
-                             'priority' => array(
-                                           'name' => 'Priorität',
-                                           'width' => 80,
-                                           'type' => 'number',
-                                           'specs' => "ClassName: 'alignright', canSort: false",
-                                           ),
-                             ),
-             'edit' => array(
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'type' => 'text',
-                                           'size' => 20,
-                                           ),
-                             'priority' => array(
-                                           'name' => 'Priorität',
-                                           'type' => 'number',
-                                           'size' => 10,
-                                           ),
-                             ),
-             );
-
-?>
index b8e9a93..d4c2a67 100644 (file)
@@ -16,14 +16,15 @@ $mask = array(
                              'theme' => array(
                                            'name' => 'Theme',
                                            'width' => 80,
-                                           'specs' => "ClassName: 'aligncenter', filterUI: 's'",
+                                           'filter' => 's',
+                                           'specs' => array('ClassName' => 'aligncenter'),
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_themes','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT theme,sys_themes.name FROM sys_theme_values JOIN sys_themes ON theme = sys_themes.id ORDER BY name",
                                            ),
                              'item' => array(
                                            'name' => 'Item',
                                            'width' => 170,
-                                           'specs' => "filterUI: 's'",
+                                           'filterUI' => 's',
                                            'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_theme_items','id','name').", 0, '')",
                                            'distinct' => "SELECT DISTINCT item,sys_theme_items.name FROM sys_theme_values JOIN sys_theme_items ON item = sys_theme_items.id ORDER BY name",
                                            ),
diff --git a/masks/system/sys_user.php b/masks/system/sys_user.php
deleted file mode 100644 (file)
index b68b091..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-$mask = array(
-             'table' => 'sys_user',
-             'title' => 'Liste der Anwender',
-             'join' => array('sys_group ON sys_user.gid = sys_group.id'),
-             'list' => array(
-                             'id' => array(
-                                           'name' => 'ID',
-                                           'visible' => false,
-                                           'sql' => 'sys_user.id',
-                                           ),
-                             'login' => array(
-                                           'name' => 'Login',
-                                           'width' => 70,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'width' => 150,
-                                           'sql' => 'sys_user.name',
-                                           ),
-                             'email' => array(
-                                           'name' => 'E-Mail',
-                                           'width' => 200,
-                                           ),
-                             'gruppe' => array(
-                                           'name' => 'Gruppe',
-                                           'width' => 70,
-                                           'sql' => 'sys_group.name',
-                                           ),
-                             'theme' => array(
-                                           'name' => 'Theme',
-                                           'width' => 90,
-                                           'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_themes','id','name').", 0, '')",
-                                           'distinct' => "SELECT DISTINCT theme,sys_themes.name FROM sys_user JOIN sys_themes ON theme = sys_themes.id ORDER BY name",
-                                           ),
-                             ),
-             'edit' => array(
-                             'login' => array(
-                                           'name' => 'Login',
-                                           'type' => 'text',
-                                           'size' => 25,
-                                           ),
-                             'name' => array(
-                                           'name' => 'Name',
-                                           'type' => 'text',
-                                           'size' => 25,
-                                           ),
-                             'email' => array(
-                                           'name' => 'E-Mail',
-                                           'type' => 'text',
-                                           'size' => 25,
-                                           ),
-                             'gid' => array(
-                                           'name' => 'Gruppe',
-                                           'type' => 'select',
-                                           'options' => 'SELECT id,name AS text FROM sys_group ORDER BY name',
-                                           ),
-                             'theme' => array(
-                                           'name' => 'Theme',
-                                           'type' => 'select',
-                                           'options' => 'SELECT id,name AS text FROM sys_themes ORDER BY name',
-                                           ),
-                             'passwd' => array(
-                                           'name' => 'Passwort',
-                                           'type' => 'passwd',
-                                           'size' => 25,
-                                           ),
-                             ),
-             );
-
-?>
diff --git a/masks/system/user.php b/masks/system/user.php
new file mode 100644 (file)
index 0000000..86b2852
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+
+JavaScript::instance()->file('lib/ricoTableColumnDB.js');
+
+$mask = array(
+             'table' => 'sys_user',
+             'title' => 'Liste der Anwender',
+             'join' => array('sys_group ON sys_user.gid = sys_group.id'),
+             'list' => array(
+                             'id' => array(
+                                           'name' => 'ID',
+                                           'visible' => false,
+                                           'sql' => 'sys_user.id',
+                                           ),
+                             'login' => array(
+                                           'name' => 'Login',
+                                           'width' => 70,
+                                           ),
+                             'name' => array(
+                                           'name' => 'Name',
+                                           'width' => 150,
+                                           'sql' => 'sys_user.name',
+                                           ),
+                             'email' => array(
+                                           'name' => 'E-Mail',
+                                           'width' => 200,
+                                           ),
+                             'gruppe' => array(
+                                           'name' => 'Gruppe',
+                                           'width' => 70,
+                                           'sql' => 'sys_group.name',
+                                           ),
+                             'theme' => array(
+                                           'name' => 'Theme',
+                                           'width' => 90,
+                                           'control' => "new Rico.TableColumn.lookup(".grid_lookup_sql('sys_themes','id','name').", 0, '')",
+                                           'distinct' => "SELECT DISTINCT theme,sys_themes.name FROM sys_user JOIN sys_themes ON theme = sys_themes.id ORDER BY name",
+                                           ),
+                             ),
+             'second' => array(
+                 'perms' => array(
+                        'title' => 'Freigaben',
+                        'table' => 'sys_group',
+                        'rows' => 10,
+                        'width' => 320,
+                        'list' => array(
+                                        'id' => array(
+                                                      'name' => 'ID',
+                                                      'visible' => false,
+                                                      'sql' => 'sys_group.id',
+                                                      ),
+                                        'gname' => array(
+                                                         'name' => 'Name',
+                                                         'width' => 80,
+                                                         'type' => 'text',
+                                                         ),
+                                        'name' => array(
+                                                        'name' => 'Name',
+                                                        'width' => 150,
+                                                        'type' => 'text',
+                                                        ),
+                                        'checked' => array(
+                                                           'name' => 'Perm',
+                                                           'width' => 40,
+                                                           'sql' => '(SELECT count(*)
+                                                               FROM sys_group_user
+                                                               WHERE sys_group_id = sys_group.id
+                                                                 AND sys_user_id = {id})',
+                                                           'control' => "new Rico.TableColumn.checkboxDB(0," .
+                                                                        "Hallinta.baseURL+'ajax/ricoUpdateConnection.php')",
+                                                           'filter' => 'c',
+                                                           'specs' => array('ClassName' => 'aligncenter has-checkbox', 'canSort' => true),
+                                                           'update' => array('table' => 'sys_group_user',
+                                                                             'basecol' => 'sys_user_id',
+                                                                             'refcol' => 'sys_group_id',
+                                                                             'reftable' => 'sys_group',
+                                                                             'refid' => 'sys_group.id',
+                                                                             ),
+                                                           ),
+                                        ),
+                                  ),
+                               ),
+             'edit' => array(
+                             'login' => array(
+                                           'name' => 'Login',
+                                           'type' => 'text',
+                                           'size' => 25,
+                                           ),
+                             'name' => array(
+                                           'name' => 'Name',
+                                           'type' => 'text',
+                                           'size' => 25,
+                                           ),
+                             'email' => array(
+                                           'name' => 'E-Mail',
+                                           'type' => 'text',
+                                           'size' => 25,
+                                           ),
+                             'gid' => array(
+                                           'name' => 'Gruppe',
+                                           'type' => 'select',
+                                           'options' => 'SELECT id,name AS text FROM sys_group ORDER BY name',
+                                           ),
+                             'theme' => array(
+                                           'name' => 'Theme',
+                                           'type' => 'select',
+                                           'options' => 'SELECT id,name AS text FROM sys_themes ORDER BY name',
+                                           ),
+                             'passwd' => array(
+                                           'name' => 'Passwort',
+                                           'type' => 'passwd',
+                                           'size' => 25,
+                                           ),
+                             ),
+             );
+
+?>
diff --git a/minimise b/minimise
new file mode 100755 (executable)
index 0000000..a524e88
--- /dev/null
+++ b/minimise
@@ -0,0 +1,34 @@
+#! /bin/bash
+
+basedir=$(dirname $0)
+
+if [ -n "$(type -path uglifyjs)" ]
+then
+    find ${basedir} -name '*.js' | grep -v '\.min\.js' | while read infile
+    do
+       outfile=${infile%.js}.min.js
+
+       if [ ! -f "$outfile" -o "$infile" -nt "$outfile" ]
+       then
+           uglifyjs "$infile" > "$outfile"
+       fi
+    done
+fi
+
+if perl -MCSS::Minifier -e '' 2>/dev/null
+then
+    find ${basedir} -name '*.css' | grep -v '\.min\.css' | while read infile
+    do
+       outfile=${infile%.css}.min.css
+
+       if [ ! -f "$outfile" -o "$infile" -nt "$outfile" ]
+       then
+           perl -MCSS::Minifier -e "
+  open(INFILE, '<', '$infile') or die;
+  open(OUTFILE, '>', '$outfile') or die;
+  CSS::Minifier::minify(input => *INFILE, outfile => *OUTFILE);
+  close(INFILE);
+  close(OUTFILE);"
+       fi
+    done
+fi
index 8e05e94..fbcd539 100644 (file)
--- a/style.css
+++ b/style.css
@@ -7,7 +7,6 @@ body {
        font: 85%/1.3 Verdana,Arial,sans-serif;
     text-align: left;
     /* background: #fff; */
-    padding-bottom:20px
 }
 
 a {
@@ -34,41 +33,82 @@ h3 {
        margin-top: 0;
 }
 
+/*
+ * Header
+ */
 div#header {
-       width:100%;
-       background: #BBD9EE;
+    width: 100%;
+    height: 3ex;
+    background: #BBD9EE;
+    background: #48b4f8;
+    background-image: url('images/bg_header.png');
+    background-repeat: repeat-x;
+    padding-bottom: 3px;
 }
 
-div#header h1, div#menu {
-       width:770px;
-       margin:0 auto;
-       text-align:left;
+div#header div#title {
+    padding-left: 5px;
+    font-size: 125%;
+    font-weight: bold;
+    float: left;
 }
 
-div#header h1 {
-    padding-top: 10px;
-    padding-bottom: 10px;
-    color: #555;
+div#header div#titlesep {
+    padding-left: 5px;
+    font-size: 125%;
+    font-weight: bold;
+    float: left;
 }
 
+div#header div#pagetitle {
+    padding-left: 5px;
+    font-size: 125%;
+    font-weight: bold;
+    float: left;
+}
 
-div.right {
-       float: right;
-       width: 76%;
-       padding: 0;
-       padding-top: 0px;
-       margin-bottom: 1.2em;
-       text-align: justify;
+div#header div#actionbox {
+    padding-top: 1px;
+    padding-right: 5px;
+    float: right;
 }
 
-div.left {
-       float: left;
-       width: 23%;
-       margin: 0;
-       padding: 0;
+div#header div#actionbox span.item {
+    margin-left: 0.3rem;
+}
+
+div#header div#actionbox span.item select {
+    font-size: 90%;
+}
+
+/*
+ * Menu
+ */
+
+div.menuDialog ul.menu {
+    list-style: none;
+    padding: 0;
+}
+div.menuDialog ul.menu li {
+   padding: 7px 7px;
+   margin: 1px;
+   border-style: solid;
+   border-width: 1px 1px 1px 0;
+   border-color: #fff #d9d9d9 #d9d9d9;
+   border-color: #fff #aaa #aaa;
+   background-color: #f6f6f6;
+   color: #000;
+}
+div.menuDialog ul.menu li.current {
+    background-color: #ddd;
+    color: #000;
+}
+div.menuDialog ul.menu li:hover {
+    background-color: #eee;
+    color: #000;
 }
 
-div.left .box {
+.box {
        padding: 0;
        margin: 0 0 1em 0;
        border: 1px solid #AAA;
@@ -80,9 +120,11 @@ p.subtitle {
 }
 
 div.content {
-       font: 95%/1.3 Verdana,Arial,sans-serif;
+       font: 97.5%/1.2 Verdana,Arial,sans-serif;
        margin: 0 auto;
-       padding: 15px;
+       padding-top: 5px;
+       padding-left: 5px;
+       padding-right: 5px;
        background: #e7f1f8;
 }
 
index 7b66729..ad9f99e 100644 (file)
@@ -10,6 +10,39 @@ h3.title, h3.title2 {
     padding-bottom: 2px;
 }
 
+/* Only for WebKit - only for mobile devices */
+@media screen and (-webkit-min-device-pixel-ratio:0)
+{
+    div.ricoLG_scrollContainerDiv {
+       background: lightgrey;
+    }
+
+    div.content {
+       padding-left: 0px !important;
+    }
+
+    div.hide-on-mobile {
+       display: none;
+    }
+    div.show-on-mobile {
+       display: unset;
+    }
+}
+
+/* @tabletViewportWidth: 48em; // 768px */
+@media screen and (max-width: 768px) {
+    div#header div#title,
+    div#header div#titlesep {
+       display: none;
+    }
+}
+@media screen and (min-width: 768px) {
+    div#header div#title,
+    div#header div#titlesep {
+       display: inline !important;
+    }
+}
+
 div.ricoLG_cell {
     padding-top: 0px;
     padding-bottom: 0px;
@@ -17,9 +50,17 @@ div.ricoLG_cell {
     cursor: default;
 }
 
+.has-checkbox div.ricoLG_cell {
+    padding-top: 0px;
+    padding-bottom: 6px;
+}
+
 div.ricoWindow {
     border: 1px solid #BBB;
 }
+div.ricoWindow .RicoPopupContent .ricoTitle .RicoCloseAnchor.ui-dialog-titlebar-close {
+    padding: 0px;
+}
 div.ricoTitle {
     background-color: #316994;
     color: white;
@@ -33,12 +74,13 @@ div.ricoContent {
     font-size: 12px;
 }
 
-div.form {
-    margin-top: 0px;
-    padding: 2px;
+div#column_grid {
+    border: none;
+}
+
+div#column_edit {
     border: 1px solid #AAA;
     color: #555;
-    overflow: hidden;
 }
 
 div.second {
@@ -48,18 +90,36 @@ div.second div.title {
     margin-bottom: 5px;
 }
 
+label {
+    display: block;
+}
 input, select {
+    display; block;
     border: 1px solid #CCC;
     color: inherit;
     background: white;
+    font-size: inherit;
 }
 
 textarea {
+    display; block;
     border: 1px solid #CCC;
     font-size: 90%;
     color: inherit;
 }
 
+input.maxwidth, select.maxwidth {
+    width: 98%
+}
+
+input[readonly] {
+    background: #EEEEEE;
+}
+
+textarea.maxwidth {
+    width: 97%
+}
+
 button {
     border: 1px solid #CCC;
     background: #DDD;
@@ -84,6 +144,12 @@ button.disabled:hover {
     background: #DDD;
 }
 
+button.builtin:disabled,
+button.custom:disabled {
+    background-image: none;
+    background-repeat: none;
+}
+
 div.buttons {
     text-align: center;
     padding-top: 0px;
@@ -106,6 +172,7 @@ img.calendar {
 p#form_status {
     font-size: 80%;
     margin-top: 0px;
+    text-align: right;
 }
 
 span.status_ok {
@@ -135,7 +202,11 @@ div#grid_info {
     color: #444;
 }
 
-div.alignrightpad {
+.gridPopup .ricoContent {
+    font-size: 125%;
+}
+
+div.alignrightpad > div {
     text-align: right;
     padding-right: 3px;
 }
@@ -147,3 +218,18 @@ div.status {
     float: right;
     text-align: right;
 }
+
+div#column_grid, div#column_edit {
+    background: #e1f3ff;
+}
+
+div#message {
+    background: white;
+    padding-left: 10px;
+    padding-right: 10px;
+    padding-top: 5px;
+    padding-bottom: 5px;
+    border: solid 1px #50a6ff;
+    background: #fff68f;
+    z-index: 200;
+}
index 7a6be93..8cbede4 100644 (file)
--- a/theme.php
+++ b/theme.php
@@ -19,7 +19,7 @@ while ($row = $sth->fetch()) {
     $styles['div.form']['background-color'] = $row['value'];
     break;
   case 'box-title-background':
-    $styles['div.ricoTitle']['background-color'] = $row['value'];
+    $styles['div.ricoTitle.ui-widget-header']['background-color'] = $row['value'];
     $styles['div.form p.title']['background-color'] = $row['value'];
     $styles['.ricoLG_table th']['background-color'] = $row['value'];
     break;