new client side Ajax admin user interface
authorTony Cook <tony@develop-help.com>
Wed, 23 Feb 2011 10:22:28 +0000 (10:22 +0000)
committerTony Cook <tony@develop-help.com>
Thu, 12 May 2011 11:20:50 +0000 (21:20 +1000)
30 files changed:
MANIFEST
site/cgi-bin/cfg/admin-ui.cfg [new file with mode: 0644]
site/cgi-bin/modules/BSE/ChangePW.pm
site/cgi-bin/modules/BSE/UI/API.pm
site/htdocs/admin/admin.html [new file with mode: 0644]
site/htdocs/css/admin-ui/admin.css [new file with mode: 0644]
site/htdocs/css/admin-ui/extra.css [new file with mode: 0644]
site/htdocs/css/admin-ui/reset.css [new file with mode: 0644]
site/htdocs/css/bse_adminui.css [new file with mode: 0644]
site/htdocs/images/admin/ui/changepw.png [new file with mode: 0644]
site/htdocs/images/admin/ui/debug.png [new file with mode: 0644]
site/htdocs/images/admin/ui/delete_dk.png [new file with mode: 0644]
site/htdocs/images/admin/ui/menu.png [new file with mode: 0644]
site/htdocs/images/admin/ui/move_dk.png [new file with mode: 0644]
site/htdocs/images/admin/ui/spinner.gif [new file with mode: 0644]
site/htdocs/js/admin-ui/debug.js [new file with mode: 0644]
site/htdocs/js/admin-ui/menu.js [new file with mode: 0644]
site/htdocs/js/bse_adminui.js [new file with mode: 0644]
site/htdocs/js/bse_api.js
site/htdocs/js/bse_dialog.js [new file with mode: 0644]
site/htdocs/js/bse_loader.js [new file with mode: 0644]
site/htdocs/js/bse_menu.js [new file with mode: 0644]
site/htdocs/js/bse_validate.js [new file with mode: 0644]
t-js/01validate.html [new file with mode: 0644]
t-js/01validate.js [new file with mode: 0644]
t-js/10menu.html [new file with mode: 0644]
t-js/10menu.js [new file with mode: 0644]
t-js/menu.css [new file with mode: 0644]
t-js/test.js [new file with mode: 0644]
t-js/tests.css [new file with mode: 0644]

index 646fa42..100ca12 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -42,6 +42,7 @@ site/cgi-bin/admin/userupdate.pl
 site/cgi-bin/affiliate.pl
 site/cgi-bin/api.pl
 site/cgi-bin/bse.cfg
+site/cgi-bin/cfg/admin-ui.cfg
 site/cgi-bin/fileprogress.fcgi
 site/cgi-bin/fileprogress.pl
 site/cgi-bin/fmail.fcgi
@@ -381,6 +382,7 @@ site/docs/upgrade_mysql.html
 site/docs/userupdate.html
 site/docs/userupdate.pod
 site/htdocs/a/.htaccess
+site/htdocs/admin/admin.html
 site/htdocs/admin/advanced.html
 site/htdocs/admin/help/access.html
 site/htdocs/admin/help/addgroup.html
@@ -396,9 +398,13 @@ site/htdocs/admin/help/subs.html
 site/htdocs/admin/help/subssend.html
 site/htdocs/admin/index.html
 site/htdocs/admin/sadmin.html
+site/htdocs/css/admin-ui/admin.css
+site/htdocs/css/admin-ui/extra.css
+site/htdocs/css/admin-ui/reset.css
 site/htdocs/css/admin.css
 site/htdocs/css/admin.css_natural
 site/htdocs/css/admin_messages.css
+site/htdocs/css/bse_adminui.css
 site/htdocs/css/sadmin.css
 site/htdocs/css/style-main.css
 site/htdocs/favicon.ico
@@ -410,6 +416,12 @@ site/htdocs/images/admin/help.gif
 site/htdocs/images/admin/move_down.gif
 site/htdocs/images/admin/move_up.gif
 site/htdocs/images/admin/nothumb.png
+site/htdocs/images/admin/ui/changepw.png
+site/htdocs/images/admin/ui/debug.png
+site/htdocs/images/admin/ui/delete_dk.png
+site/htdocs/images/admin/ui/menu.png
+site/htdocs/images/admin/ui/move_dk.png
+site/htdocs/images/admin/ui/spinner.gif
 site/htdocs/images/admin/unchecked.gif
 site/htdocs/images/filestatus/download.gif
 site/htdocs/images/filestatus/forSale.gif
@@ -431,12 +443,19 @@ site/htdocs/images/titles/the_shop.gif
 site/htdocs/images/titles/your_site.gif
 site/htdocs/images/trans_pixel.gif
 site/htdocs/images/videoclose.png
+site/htdocs/js/admin-ui/debug.js
+site/htdocs/js/admin-ui/menu.js
 site/htdocs/js/admin_messages.js
 site/htdocs/js/admin_prodopts.js
 site/htdocs/js/admin_tools.js
 site/htdocs/js/bse.js
+site/htdocs/js/bse_adminui.js
 site/htdocs/js/bse_api.js
+site/htdocs/js/bse_dialog.js
 site/htdocs/js/bse_flowplayer.js
+site/htdocs/js/bse_loader.js
+site/htdocs/js/bse_menu.js
+site/htdocs/js/bse_validate.js
 site/htdocs/js/builder.js
 site/htdocs/js/controls.js
 site/htdocs/js/date.js
@@ -737,6 +756,13 @@ site/util/make_versions.pl
 site/util/mysql.str
 site/util/update_title_summary.pl
 site/util/upgrade_mysql.pl
+t-js/01validate.html
+t-js/01validate.js
+t-js/10menu.html
+t-js/10menu.js
+t-js/menu.css
+t-js/test.js
+t-js/tests.css
 t/BSE/Test.pm
 t/cfg/bse.cfg
 t/cfg/cfg/00start.cfg
diff --git a/site/cgi-bin/cfg/admin-ui.cfg b/site/cgi-bin/cfg/admin-ui.cfg
new file mode 100644 (file)
index 0000000..4686162
--- /dev/null
@@ -0,0 +1,6 @@
+[extra a_config]
+admin_ui=admin ui scripts
+
+[admin ui scripts]
+menu=Main Menu;/js/admin-ui/menu.js;am
+debug=Debug Log;/js/admin-ui/debug.js;zm
index c747e5f..5aa3e71 100644 (file)
@@ -4,7 +4,7 @@ use BSE::Util::Tags qw(tag_error_img);
 use BSE::Util::HTML;
 use base 'BSE::UI::AdminDispatch';
 
-our $VERSION = "1.000";
+our $VERSION = "1.001";
 
 my %actions =
   (
@@ -78,12 +78,18 @@ sub req_change {
       && $newpw ne $confirm) {
     $errors{confirm} = "Confirmation password does not match new password";
   }
-  keys %errors
-    and return $class->req_form($req, undef, \%errors);
+  if (keys %errors) {
+    $req->is_ajax
+      and return $class->_field_error($req, \%errors);
+    return $class->req_form($req, undef, \%errors);
+  }
 
   $user->changepw($newpw);
   $user->save;
 
+  $req->is_ajax
+    and return $req->json_content(success => 1);
+
   my $r = $cgi->param('r');
   unless ($r) {
     $r = $req->url('menu', { m => "New password saved" });
index 4d2979a..ed083e0 100644 (file)
@@ -2,7 +2,7 @@ package BSE::UI::API;
 use strict;
 use base "BSE::UI::Dispatch";
 
-our $VERSION = "1.000";
+our $VERSION = "1.001";
 
 my %actions =
   (
@@ -22,13 +22,28 @@ sub req_config {
   my ($self, $req) = @_;
 
   my $cfg = $req->cfg;
-  return $req->json_content
+  my %result =
     (
      success => 1,
      perlbal => $cfg->entry("basic", "perlbal", 0),
      access_control => $cfg->entry("basic", "access_control", 0),
      tracking_uploads => $req->_tracking_uploads,
     );
+
+  my %custom = $cfg->entries("extra a_config");
+  for my $key (keys %custom) {
+    exists $result{$key} and next;
+
+    my $section = $custom{$key};
+    $section =~ /\{(level|generator|parentid|template)\}/
+      and next;
+
+    $section eq "db" and die;
+
+    $result{$key} = { $cfg->entries($section) };
+  }
+
+  return $req->json_content(\%result);
 }
 
 sub req_fail {
diff --git a/site/htdocs/admin/admin.html b/site/htdocs/admin/admin.html
new file mode 100644 (file)
index 0000000..78391b5
--- /dev/null
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+       <meta charset="utf-8" />
+       <title>BSE Administration</title>
+    <script src="/js/prototype.js" type="text/javascript"></script>
+    <script src="/js/scriptaculous.js" type="text/javascript"></script>
+    <script src="/js/bse_api.js" type="text/javascript"></script>
+    <script src="/js/bse_validate.js" type="text/javascript"></script>
+    <script src="/js/bse_dialog.js" type="text/javascript"></script>
+    <script src="/js/bse_loader.js" type="text/javascript"></script>
+    <script src="/js/bse_menu.js" type="text/javascript"></script>
+    <script src="/js/bse_adminui.js" type="text/javascript"></script>
+<link rel="stylesheet" type="text/css" href="/css/admin-ui/reset.css" />
+<link rel="stylesheet" type="text/css" href="/css/admin-ui/admin.css" />
+<link rel="stylesheet" type="text/css" href="/css/admin-ui/extra.css" />
+</head>
+<body>
+  <!-- div id="lightbox" class="lightbox"></div -->
+  <nav id="menu" class="menu">
+    <ul id="nav" class="nav">
+    </ul>
+    <p id="message" class="message green"></p>
+  </nav>
+
+  <div id="base_work" class="preview"></div>
+</body>
+
+</html>
diff --git a/site/htdocs/css/admin-ui/admin.css b/site/htdocs/css/admin-ui/admin.css
new file mode 100644 (file)
index 0000000..12632f5
--- /dev/null
@@ -0,0 +1,680 @@
+/* @override 
+       http://www.spacepark.com/admin/sp/css/admin.css
+       file:///Users/adriann/Work/Spacepark/spadmin/admin.css
+*/
+
+* {
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+}
+html {
+    text-align: center;
+    width: 100%;
+}
+body {
+       width: 100%;
+    text-align: left;
+    margin: 0 auto;
+    padding: 0;
+    background-color: #666;
+    font: normal normal 0.75em/1.5 "Lucida Sans Unicode", "Lucida Grande", "Helvetica Neue", Helvetica, Arial, Helvetica, sans-serif;
+    text-shadow: 0 0.083em 0 #fff;
+    color: #585858;
+       position: relative;
+}
+select, input, textarea, button {
+       font-size: 1em;
+       margin: 0;
+}
+.hidden {
+    display: none !important;
+}
+.left {
+       float: left;
+}
+.right {
+       float: right;
+}
+.preview {
+       position: absolute;
+       top: 4em;
+       right: 0;
+       bottom: 0;
+       left: 0;
+    z-index: 1;
+    position: fixed;
+}
+iframe {
+    width: 100%;
+    height: 100%;
+    border: 0; /* ie requires HTML iframe attribute frameborder="0" */
+    z-index: 1;
+    position: absolute;
+}
+.lightbox {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    margin: 0;
+    padding: 0;
+    background-color: rgba(0,0,0,0.75);
+    z-index: 3;
+    overflow: visible;
+    height: 100%;
+    width: 100%;
+}
+.half {
+       width: 50%;
+}
+.full {
+       width: 100%;
+}
+fieldset.error, label.error, .error>label, .error>fieldset, .error>legend {
+       color: #c03 !important;
+       border-color: #c03 !important;
+}
+
+/* @group window */
+
+div.window {
+       position: relative;
+       z-index: 100;
+    margin: 4em auto;
+    margin-top: 8em;
+    width: 95%;
+       max-width: 80em;
+       min-width: 34em;
+    padding: 1em 2em 1.5em;
+    background-color: #e8e8e8;
+       border-top: 0.2em solid #fff;
+
+       overflow: hidden; /* clear contained floats */
+
+       -webkit-border-radius: 1em;
+       -moz-border-radius: 1em;
+       -o-border-radius: 1em;
+       border-radius: 1em;
+
+       -moz-box-shadow: 0.25em 0.25em 0.5em rgba(0,0,0,0.75);
+       -webkit-box-shadow: 0.25em 0.25em 0.5em rgba(0,0,0,0.75);
+       -o-box-shadow: 0.25em 0.25em 0.5em rgba(0,0,0,0.75);
+       box-shadow: 0.25em 0.25em 0.5em rgba(0,0,0,0.75);
+}
+div.window.dialog {
+       max-width: 40em;
+}
+div.window fieldset fieldset {
+       /*display: table; opera encloses the legend if set as table */
+       border: 0.1em solid #ddd;
+    border-color: #b5b5b5 #ddd #fff #ddd;
+       margin-bottom: 1em;
+       padding: 1em;
+       padding-bottom: 0.75em;
+    
+       -webkit-border-radius: 0.5em;
+       -moz-border-radius: 0.5em;
+       -o-border-radius: 0.5em;
+       border-radius: 0.5em;
+}
+div.window>form>fieldset>legend {
+    font-size: 2em !important;
+    color: #999;
+}
+div.window fieldset fieldset legend {
+    font-size: 1.25em;
+    color: #333;
+    margin: 0;
+    padding: 0 0.25em;
+}
+div.window fieldset>div {
+       display: table-row;
+}
+div.window fieldset>div>label, div.window fieldset>div>span {
+    display: table-cell;
+    vertical-align: top;
+    /*padding: 0.25em 1em 0.25em 0;*/
+    padding: 0.25em 0;
+}
+div.window fieldset>div>label {
+       padding-right: 0.5em;
+    text-align: right;
+    width: 10%;
+    min-width: 9em;
+    white-space: nowrap;
+}
+div.window fieldset>div>span {
+       width: 100%;
+}
+div.window fieldset>div>span>button {
+    display: none;
+    /*background-color: #ccc;
+       width: 1.75em;
+       height: 1.75em;
+       text-indent: -9999em;
+       overflow: hidden;*/
+}
+div.window fieldset>div:hover>span>button {
+    display: inline;
+}
+div.window fieldset input[type=text], div.window fieldset input[type=password], 
+div.window fieldset input[type=file], div.window fieldset select, div.window fieldset textarea {
+    border: 0.1em solid #ddd;
+    border-top-color: #aaa;
+    line-height: 1.2;
+    padding: 0.25em;
+    background-color: #eee;
+    float: left; /* because some odd margins persist 
+    display: inline-block;*/
+    min-width: 20em;
+    width: 40%;
+}
+div.window fieldset input[type=file] {
+    padding: 0.1em 0.25em;
+    overflow: hidden;
+    position: relative;
+}
+div.window fieldset div.full input[type=text], div.window fieldset div.full input[type=password], 
+div.window fieldset div.full input[type=file], div.window fieldset div.full select, div.window fieldset div.full textarea,
+div.dialog fieldset input[type=text], div.dialog fieldset input[type=password], 
+div.dialog fieldset input[type=file], div.dialog fieldset select, div.dialog fieldset textarea {
+       width: 100%;
+}
+div.window fieldset textarea {
+    height: 20em;
+}
+div.window.dialog fieldset textarea {
+    height: 10em;
+}
+div.window fieldset input:focus, div.window fieldset textarea:focus, div.window fieldset select:focus {
+       border-color: rgba(3,153,212,0.75);
+       -moz-box-shadow: 0 0 0.25em rgba(3,153,212,0.75);
+       -webkit-box-shadow: 0 0 0.25em rgba(3,153,212,0.75);
+       -o-box-shadow: 0 0 0.25em rgba(3,153,212,0.75);
+       box-shadow: 0 0 0.25em rgba(3,153,212,0.75);
+}
+div.window fieldset>label {
+       text-align: left;
+       display: block;
+}
+
+/* @end */
+
+/* @group menu */
+
+#menu p.message {
+       position: absolute;
+       top: 4em;
+       width: 100%;
+       padding: 1em 2em;
+       color: #fff;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.3);
+       font-weight: bold;
+
+       -webkit-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       -moz-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+}
+#menu {
+       margin: 0;
+       padding: 0;
+       line-height: 1;
+       width: 100%;
+       height: 4em;
+       
+       -webkit-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       -moz-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+
+       background: #8b8b8b; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#a9a9a9', endColorstr='#7a7a7a'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#a9a9a9), to(#7a7a7a)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #a9a9a9, #7a7a7a); /* for firefox 3.6+ */
+
+       border-bottom: solid 0.1em #6d6d6d;
+       
+       z-index: 2;
+       position: fixed;
+       top: 0;
+       overflow: visible;
+}
+#nav {
+       position: relative;
+       z-index: 1001;
+       overflow: visible;
+}
+#nav li {
+       margin: 0 0 0 1em;
+       padding: 0.75em 0;
+       float: left;
+       position: relative;
+       list-style: none;
+}
+#nav li li.separate {
+       border-bottom: 0.1em solid #b4b4b4;
+}
+#nav li li.separate+li {
+       border-top: 0.1em solid #fff;
+}
+/* main level link */
+#nav a, #nav li > span {
+       position: relative;
+       line-height: 1.5;
+       font-weight: bold;
+       color: #e7e5e5;
+       text-decoration: none;
+       display: block;
+       margin: -0.1em 0;
+       padding: 0.5em 1.5em;
+       -webkit-border-radius: 1.5em;
+       -moz-border-radius: 1.5em;
+       border-radius: 1.5em;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.5);
+}
+/* main level link hover */
+#nav .current > a, #nav li:hover > a,
+#nav .current > span, #nav li:hover > span {
+       background: #d1d1d1; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb', endColorstr='#a1a1a1'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#a1a1a1)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #ebebeb, #a1a1a1); /* for firefox 3.6+ */
+
+       color: #444;
+       border-top: solid 0.1em #f8f8f8;
+       -webkit-box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       -moz-box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       text-shadow: 0 0.1em 0.1em rgba(255,255,255,1);
+}
+/* sub levels link hover */
+#nav ul li:hover a, #nav li:hover li a,
+#nav ul li:hover>span, #nav li:hover li>span {
+       background: none;
+       border: none;
+       color: #666;
+       -webkit-box-shadow: none;
+       -moz-box-shadow: none;
+       box-shadow: none;
+       margin: 0;
+}
+#nav ul a:hover, #nav ul li>span:hover {
+       background: #0399d4 !important; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#04acec', endColorstr='#0186ba'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#04acec), to(#0186ba)) !important; /* for webkit browsers */
+       background: -moz-linear-gradient(top, #04acec, #0186ba) !important; /* for firefox 3.6+ */
+
+       color: #fff !important;
+       -webkit-border-radius: 0;
+       -moz-border-radius: 0;
+       border-radius: 0;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.1);
+}
+/* level 2 list */
+#nav ul {
+       background: #ddd; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#cfcfcf'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#cfcfcf)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #fff, #cfcfcf); /* for firefox 3.6+ */
+
+       z-index: 100;
+       display: none;
+       margin: 0;
+       padding: 0;
+       width: 18em;
+       position: absolute;
+       top: 3.75em;
+       left: 0;
+       border: solid 0.1em #b4b4b4;
+       -webkit-border-radius: 0.5em;
+       -moz-border-radius: 0.5em;
+       -o-border-radius: 0.5em;
+       border-radius: 0.5em;
+       -webkit-box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+       -moz-box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+       box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+}
+#nav ul ul {
+       max-height: 30.16em;
+       overflow-y: auto;
+       overflow-x: visible;
+}
+#nav ul.full {
+       width: 27em;
+}
+
+/* @group widgets */
+
+#nav li>a>span, #nav li>span>span {
+       position: absolute;
+       top: 0;
+       left: 0;
+       width: 1.2em;
+       height: 100%;
+       text-indent: -9999em;
+       background: transparent url(../images/move_dk.png) no-repeat 50% 50%;
+       background-size: 1.2em;
+       display: none;
+}
+#nav li>a>span.delete, #nav li>span>span.delete {
+       background-image: url(../images/delete_dk.png);
+       background-size: 1.5em;
+       width: 1.5em;
+       left: auto;
+       right: 0.5em;
+}
+#nav li:hover>a>span, #nav li:hover>span>span {
+       display: block;
+}
+
+/* @end */
+
+/* @group dropdown */
+
+#nav li:hover > ul {
+       display: block;
+}
+#nav ul li {
+       float: none;
+       margin: 0;
+       padding: 0;
+}
+#nav ul a, #nav ul li>span {
+       font-weight: normal;
+       text-shadow: 0 0.1em 0.1em rgba(255,255,255,0.9);
+}
+
+/* @end */
+
+/* @group level 3+ list */
+
+#nav ul ul {
+       left: 17.5em;
+       top: -0.1em;
+}
+/*#nav ul ul.full {
+       left: 35.5em;
+}*/
+
+/* @end */
+
+/* @group rounded corners for first and last child */
+
+#nav ul li:first-child > a {
+       -webkit-border-top-left-radius: 0.5em;
+       -moz-border-radius-topleft: 0.5em;
+       border-top-left-radius: 0.5em;
+       -webkit-border-top-right-radius: 0.5em;
+       -moz-border-radius-topright: 0.5em;
+       border-top-right-radius: 0.5em;
+}
+#nav ul li:last-child > a {
+       -webkit-border-bottom-left-radius: 0.5em;
+       -moz-border-radius-bottomleft: 0.5em;
+       border-bottom-left-radius: 0.5em;
+       -webkit-border-bottom-right-radius: 0.5em;
+       -moz-border-radius-bottomright: 0.5em;
+       border-bottom-right-radius: 0.5em;
+}
+
+/* @end */
+
+/* @end */
+
+/* @group button */
+
+
+/* button 
+---------------------------------------------- */
+button[disabled], button[disabled]:hover, button[disabled]:active {
+       opacity: 0.25;
+       top: 0;
+       color: #e9e9e9;
+       border: solid 0.1em #555;
+       background: #6e6e6e;
+       background: -webkit-gradient(linear, left top, left bottom, from(#888), to(#575757));
+       background: -moz-linear-gradient(top, #888, #575757);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#888888', endColorstr='#575757');
+}
+.button {
+       display: inline-block;
+       zoom: 1; /* zoom and *display = ie7 hack for display:inline-block */
+       *display: inline;
+       vertical-align: top;
+       margin: 0 0 0 0.5em;
+       padding: 0.5em 1.5em;
+
+       cursor: pointer;
+       text-align: center;
+       text-decoration: none;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.3);
+       -webkit-border-radius: 0.5em; 
+       -moz-border-radius: 0.5em;
+       border-radius: 0.5em;
+       -webkit-box-shadow: 0 0.1em 0.2em rgba(0,0,0,0.5);
+       -moz-box-shadow: 0 0.1em 0.2em rgba(0,0,0,0.5);
+       box-shadow: 0 0.1em 0.2em rgba(0,0,0,0.5);
+       border-width: 0.1em !important;
+
+       line-height: 1.25; /* Cannot modify because FF doesn't allow override on button element */
+       color: #fff !important;
+}
+.button:hover {
+       text-decoration: none;
+}
+.button:active {
+       position: relative;
+       top: 0.1em;
+}
+.bigrounded {
+       -webkit-border-radius: 2em;
+       -moz-border-radius: 2em;
+       border-radius: 2em;
+}
+.small {
+       font-size: 0.85em;
+       padding: 0.3em 1em;
+}
+
+/* color styles 
+---------------------------------------------- */
+
+/* black */
+.black {
+       color: #d7d7d7;
+       border: solid 0.1em #333;
+       background: #333;
+       background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#000));
+       background: -moz-linear-gradient(top, #666, #000);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#666666', endColorstr='#000000');
+}
+.black:hover {
+       background: #000;
+       background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#000));
+       background: -moz-linear-gradient(top, #444, #000);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#444444', endColorstr='#000000');
+}
+.black:active {
+       color: #666;
+       background: -webkit-gradient(linear, left top, left bottom, from(#000), to(#444));
+       background: -moz-linear-gradient(top, #000, #444);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#000000', endColorstr='#666666');
+}
+
+/* gray */
+.gray {
+       color: #e9e9e9;
+       border: solid 0.1em #555;
+       background: #6e6e6e;
+       background: -webkit-gradient(linear, left top, left bottom, from(#888), to(#575757));
+       background: -moz-linear-gradient(top, #888, #575757);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#888888', endColorstr='#575757');
+}
+.gray:hover {
+       background: #616161;
+       background: -webkit-gradient(linear, left top, left bottom, from(#757575), to(#4b4b4b));
+       background: -moz-linear-gradient(top, #757575, #4b4b4b);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#757575', endColorstr='#4b4b4b');
+}
+.gray:active {
+       color: #afafaf;
+       background: -webkit-gradient(linear, left top, left bottom, from(#575757), to(#888));
+       background: -moz-linear-gradient(top, #575757, #888);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#575757', endColorstr='#888888');
+}
+
+/* orange */
+.orange {
+       color: #fef4e9;
+       border: solid 0.1em #da7c0c;
+       background: #f78d1d;
+       background: -webkit-gradient(linear, left top, left bottom, from(#faa51a), to(#f47a20));
+       background: -moz-linear-gradient(top, #faa51a, #f47a20);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#faa51a', endColorstr='#f47a20');
+}
+.orange:hover {
+       background: #f47c20;
+       background: -webkit-gradient(linear, left top, left bottom, from(#f88e11), to(#f06015));
+       background: -moz-linear-gradient(top, #f88e11, #f06015);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#f88e11', endColorstr='#f06015');
+}
+.orange:active {
+       color: #fcd3a5;
+       background: -webkit-gradient(linear, left top, left bottom, from(#f47a20), to(#faa51a));
+       background: -moz-linear-gradient(top, #f47a20, #faa51a);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#f47a20', endColorstr='#faa51a');
+}
+
+/* red */
+.red {
+       color: #faddde;
+       border: solid 0.1em #980c10;
+       background: #d81b21;
+       background: -webkit-gradient(linear, left top, left bottom, from(#ed1c24), to(#aa1317));
+       background: -moz-linear-gradient(top, #ed1c24, #aa1317);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ed1c24', endColorstr='#aa1317');
+}
+.red:hover {
+       background: #b61318;
+       background: -webkit-gradient(linear, left top, left bottom, from(#c9151b), to(#a11115));
+       background: -moz-linear-gradient(top, #c9151b, #a11115);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#c9151b', endColorstr='#a11115');
+}
+.red:active {
+       color: #de898c;
+       background: -webkit-gradient(linear, left top, left bottom, from(#aa1317), to(#ed1c24));
+       background: -moz-linear-gradient(top, #aa1317, #ed1c24);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#aa1317', endColorstr='#ed1c24');
+}
+
+/* blue */
+.blue {
+       color: #d9eef7;
+       border: solid 0.1em #0076a3;
+       background: #0095cd;
+       background: -webkit-gradient(linear, left top, left bottom, from(#00adee), to(#0078a5));
+       background: -moz-linear-gradient(top, #00adee, #0078a5);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#00adee', endColorstr='#0078a5');
+}
+.blue:hover {
+       background: #007ead;
+       background: -webkit-gradient(linear, left top, left bottom, from(#0095cc), to(#00678e));
+       background: -moz-linear-gradient(top, #0095cc, #00678e);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#0095cc', endColorstr='#00678e');
+}
+.blue:active {
+       color: #80bed6;
+       background: -webkit-gradient(linear, left top, left bottom, from(#0078a5), to(#00adee));
+       background: -moz-linear-gradient(top, #0078a5, #00adee);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#0078a5', endColorstr='#00adee');
+}
+
+/* rosy */
+.rosy {
+       color: #fae7e9;
+       border: solid 0.1em #b73948;
+       background: #da5867;
+       background: -webkit-gradient(linear, left top, left bottom, from(#f16c7c), to(#bf404f));
+       background: -moz-linear-gradient(top, #f16c7c, #bf404f);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#f16c7c', endColorstr='#bf404f');
+}
+.rosy:hover {
+       background: #ba4b58;
+       background: -webkit-gradient(linear, left top, left bottom, from(#cf5d6a), to(#a53845));
+       background: -moz-linear-gradient(top, #cf5d6a, #a53845);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#cf5d6a', endColorstr='#a53845');
+}
+.rosy:active {
+       color: #dca4ab;
+       background: -webkit-gradient(linear, left top, left bottom, from(#bf404f), to(#f16c7c));
+       background: -moz-linear-gradient(top, #bf404f, #f16c7c);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#bf404f', endColorstr='#f16c7c');
+}
+
+/* green */
+.green {
+       color: #e8f0de;
+       border: solid 0.1em #538312;
+       background: #64991e;
+       background: -webkit-gradient(linear, left top, left bottom, from(#7db72f), to(#4e7d0e));
+       background: -moz-linear-gradient(top, #7db72f, #4e7d0e);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#7db72f', endColorstr='#4e7d0e');
+}
+.green:hover {
+       background: #538018;
+       background: -webkit-gradient(linear, left top, left bottom, from(#6b9d28), to(#436b0c));
+       background: -moz-linear-gradient(top, #6b9d28, #436b0c);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#6b9d28', endColorstr='#436b0c');
+}
+.green:active {
+       color: #a9c08c;
+       background: -webkit-gradient(linear, left top, left bottom, from(#4e7d0e), to(#7db72f));
+       background: -moz-linear-gradient(top, #4e7d0e, #7db72f);
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#4e7d0e', endColorstr='#7db72f');
+}
+
+/* @end */
+
+/* @group buttons */
+
+.buttons {
+       display: table;
+       width: 100%;
+    text-align: right;
+    clear: both;
+    margin-top: 1em;
+}
+.buttons>span {
+       display: table-cell;
+       white-space: nowrap;
+       vertical-align: top;
+}
+.buttons>span span {
+       text-align: left;
+       text-indent: -9999em;
+       height: 2.5em;
+       border-width: 0.1em !important;
+}
+.buttons span.progress {
+       background-color: #bbb;
+       width: 100%;
+}
+.buttons>span.progress span {
+       display: block;
+}
+.buttons>span span.status {
+       text-indent: 0.5em;
+       line-height: 2.5;
+       margin-bottom: -2.5em;
+       color: #fff;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.3);
+}
+.buttons>span span.spinner {
+       display: inline-block;
+       width: 2.5em;
+       background: transparent url(../images/spinner.gif) no-repeat 100% 50%;
+       background-size: 2em;
+}
+
+/* @end */
diff --git a/site/htdocs/css/admin-ui/extra.css b/site/htdocs/css/admin-ui/extra.css
new file mode 100644 (file)
index 0000000..1740e16
--- /dev/null
@@ -0,0 +1,119 @@
+/* functions not covered by Adrian's base classes.
+   I don't want to modify the original, since he'll make refinements.
+*/
+.bse_modal {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    margin: 0;
+    padding: 0;
+    background-color: rgba(0,0,0,0.75);
+    z-index: 3;
+    overflow: visible;
+    height: 100%;
+    width: 100%;
+}
+
+#base_work {
+    color: #FFF;
+}
+
+.buttons>span span.spinner {
+  background: transparent url(/images/admin/ui/spinner.gif) no-repeat 100% 50%;
+}
+
+#nav li>span>span.item { 
+  display: block;
+  text-indent: 0em;
+  position: static;
+  width: auto;
+}
+
+#nav li>span>span.move {
+       background: transparent url(/images/admin/ui/move_dk.png) no-repeat 50% 50%;
+              
+}
+
+#nav li>span>span.delete {
+       background-image: url(/images/admin/ui/delete_dk.png);
+}
+
+/* undo the CSS popup styling */
+#nav ul, #nav li>a>span, #nav li>span>span {
+  display: block;
+}
+
+.bse_field_error {
+  margin-top: 2em;
+  margin-left: -25em;
+  color: #F00;
+}
+
+.bse_image_field img.display {
+  width: 80px;
+  height: 80px;
+  border: 1px solid #CCC;
+  float: right;
+}
+
+div.window fieldset.bse_image_field > input[type=file] {
+  width: auto;
+}
+
+div.window fieldset.bse_image_field > div.more {
+  clear: both;
+  display: block;
+}
+
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist {
+  border: 1px solid #808080;
+  display: block;
+  height: 102px;
+}
+
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist .bse_gallery_image,
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist .bse_drop_target {
+  width: 81px;
+  height: 100px;
+  float: left;
+  border-right: 1px solid #808080;
+  position: relative;
+}
+
+.bse_gallery_image {
+  background-color: #FFF;
+}
+
+.bse_drop_target { 
+  background-color: #CFC;
+  font-weight: bold;
+  padding-top: 40px;
+  text-align: center;
+}
+
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist > .bse_gallery_image .name {
+  width: 80px;
+  position: absolute;
+  top: 80px;
+  display: block;
+  text-align: center;
+  overflow: hidden;
+  text-size: 70%;
+}
+
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist > .bse_gallery_image .delete {
+  top: 0px;
+  right: 0px;
+  position: absolute;
+  background-color: #CCC;
+}
+
+div.window fieldset.bse_image_gallery div.bse_gallery_imagelist > .bse_gallery_image .edit {
+  top: 1.5em;
+  right: 0px;
+  position: absolute;
+  background-color: #CCC;
+}
+
diff --git a/site/htdocs/css/admin-ui/reset.css b/site/htdocs/css/admin-ui/reset.css
new file mode 100644 (file)
index 0000000..99a0211
--- /dev/null
@@ -0,0 +1 @@
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}
\ No newline at end of file
diff --git a/site/htdocs/css/bse_adminui.css b/site/htdocs/css/bse_adminui.css
new file mode 100644 (file)
index 0000000..d018abb
--- /dev/null
@@ -0,0 +1,191 @@
+#debug_log {
+}
+
+body {
+  font: 12px Arial;
+  margin: 0px;
+  background-color: #E0E0E0;
+}
+
+.bse_modal {
+  position: fixed;
+  top: 0px;
+  background-image: url(/images/50.png);
+  width: 100%;
+  height: 20em;
+}
+
+.bse_dialog .bse_title {
+  text-align: center;
+  font-weight: bold;
+  border-bottom: 1px solid #8080FF;
+}
+
+.bse_dialog .bse_alert {
+  background-color: #F00;
+  color: #FFF;
+}
+
+.bse_dialog {
+  position: absolute;
+  background-color: #F0F0FF;
+  padding: 5px;
+  border: 2px solid #FFF;
+}
+
+.bse_dialog .bse_required label:after {
+  content: "*";
+}
+
+.bse_dialog .bse_invalid { 
+  background-color: #FF8080;
+}
+
+.bse_error {
+  padding: 0px 3px;
+  font-weight: bold;
+  color: red;
+}
+
+.bse_dialog input,
+.bse_dialog textarea,
+.bse_dialog select {
+
+}
+
+.bse_field_wrapper{
+  margin-left: 200px;
+  margin-top: 10px;
+}
+
+.bse_dialog label {
+  margin-left: -190px;
+  float: left;
+}
+
+#base_wrapper {
+  width: 1010px;
+  border-left: 5px solid white;
+  border-right: 5px solid white;
+  margin-left: auto;
+  margin-right: auto;
+  background-color: #F0F0FF;
+}
+
+#base_menubar {
+  background-color: #E0E0FF;
+  border-bottom: 1px solid #8080FF;
+  /*position: fixed;
+  width: 1000px; */
+}
+
+#base_menu_wrapper {
+  position: relative;
+  background-color: #C0C0FF;
+  width: 150px;
+  float: left;
+}
+
+#base_menu {
+  display: none;
+  background-color: #C0C0FF;
+  position: absolute;
+  top: 2em;
+  left: 1px;
+  /*border-top: 1px solid #000;*/
+}
+
+#base_menu_wrapper:hover #base_menu {
+  display: block;
+}
+
+#base_menu_current {
+  padding: 0.5em 1em;
+  white-space: nowrap;
+  overflow: hidden;
+  width: 150px;
+  font-weight: bold;
+  cursor: pointer;
+}
+
+#base_menu a,
+#base_logon_menu a {
+  display: block;
+  padding: 5px 1em;
+  /*border-bottom: 1px solid #000;
+  border-left: 1 px solid #000;
+  border-right: 1px solid #000;*/
+  text-decoration: none;
+  font-weight: bold;
+  white-space: nowrap;
+  width: 150px;
+  background-position: 150px;
+  background-repeat: no-repeat;
+}
+
+#base_menu a:hover,
+#base_logon_menu a:hover {
+  background-color: #E0E0FF;
+}
+
+#base_menu_item_debug {
+  background-image: url(/images/admin/ui/debug.png);
+}
+
+#base_menu_item_menu {
+  background-image: url(/images/admin/ui/menu.png);
+}
+
+#base_change_password {
+  background-image: url(/images/admin/ui/changepw.png);
+}
+
+#base_logon_wrapper {
+  position: relative;
+  background-color: #C0C0FF;
+  float: right;
+}
+
+#base_logon {
+  display: none;
+  font-weight: bold;
+  background-color: #C00000;
+  color: #FFFFFF;
+  float: right;
+  padding: 0.5em 1em;
+  cursor: pointer;
+}
+
+#base_logon_menu {
+  display: none;
+  background-color: #C0C0FF;
+  position: absolute;
+  right: 0px;
+  top: 100%;
+}
+
+#base_logon_wrapper:hover #base_logon_menu {
+  display: block;
+}
+
+.clearer { clear: both; }
+
+/* hide the various page components until initialized */
+#base_wrapper.hide #base_menubar,
+#base_wrapper.hide #base_work {
+  display: none;
+}
+
+#base_loading {
+  text-align: center;
+  font: bold 24px Arial;
+  padding-top: 10em;
+  padding-bottom: 10em;
+  color: #8080FF;
+}
+
+#base_messages div.message {
+  background-color: #C0C0C0;
+  padding: 2px 5px;
+}
+
diff --git a/site/htdocs/images/admin/ui/changepw.png b/site/htdocs/images/admin/ui/changepw.png
new file mode 100644 (file)
index 0000000..09bc58d
Binary files /dev/null and b/site/htdocs/images/admin/ui/changepw.png differ
diff --git a/site/htdocs/images/admin/ui/debug.png b/site/htdocs/images/admin/ui/debug.png
new file mode 100644 (file)
index 0000000..f398077
Binary files /dev/null and b/site/htdocs/images/admin/ui/debug.png differ
diff --git a/site/htdocs/images/admin/ui/delete_dk.png b/site/htdocs/images/admin/ui/delete_dk.png
new file mode 100644 (file)
index 0000000..5c31bfa
Binary files /dev/null and b/site/htdocs/images/admin/ui/delete_dk.png differ
diff --git a/site/htdocs/images/admin/ui/menu.png b/site/htdocs/images/admin/ui/menu.png
new file mode 100644 (file)
index 0000000..386e09c
Binary files /dev/null and b/site/htdocs/images/admin/ui/menu.png differ
diff --git a/site/htdocs/images/admin/ui/move_dk.png b/site/htdocs/images/admin/ui/move_dk.png
new file mode 100644 (file)
index 0000000..7d2aab8
Binary files /dev/null and b/site/htdocs/images/admin/ui/move_dk.png differ
diff --git a/site/htdocs/images/admin/ui/spinner.gif b/site/htdocs/images/admin/ui/spinner.gif
new file mode 100644 (file)
index 0000000..46025be
Binary files /dev/null and b/site/htdocs/images/admin/ui/spinner.gif differ
diff --git a/site/htdocs/js/admin-ui/debug.js b/site/htdocs/js/admin-ui/debug.js
new file mode 100644 (file)
index 0000000..e285d5d
--- /dev/null
@@ -0,0 +1,34 @@
+var BSEDebugUI = Class.create
+(BSEUIBase,
+{
+  start: function(ui, div, args) {
+    div.innerHTML = "";
+    this._log = new Element("div", { id: "debug_log" });
+    div.appendChild(this._log);
+    this._load_log(ui);
+    this.display(ui, div);
+  },
+  display: function(ui, div) {
+    this._timer = setInterval(this._load_log.bind(this, ui), 1000);
+  },
+  undisplay: function(ui, div) {
+    clearInterval(this._timer);
+  },
+  needed_content: function(ui, args) {
+    return { };
+  },
+  _load_log: function(ui) {
+    this._log.innerHTML = "";
+    for (var i = 0; i < ui._log.length; ++i) {
+      var entry = new Element("div");
+      entry.appendChild(document.createTextNode(ui._log[i]));
+      this._log.appendChild(entry);
+    }
+  }
+});
+
+ui.register({
+  name: "debug",
+  object: new BSEDebugUI(),
+  logon: false
+});
\ No newline at end of file
diff --git a/site/htdocs/js/admin-ui/menu.js b/site/htdocs/js/admin-ui/menu.js
new file mode 100644 (file)
index 0000000..65fa8e8
--- /dev/null
@@ -0,0 +1,17 @@
+var BSEMenuUI = Class.create
+(BSEUIBase,
+{
+  start: function(ui, div, args) {
+    div.innerHTML = "There will be a menu here";
+  },
+    display: function(ui, div) {
+    },
+  needed_content: function(ui, args) {
+    return { menu: "/admin/ui/menu.html" };
+  }
+});
+
+ui.register({
+  name: "menu",
+  object: new BSEMenuUI
+});
\ No newline at end of file
diff --git a/site/htdocs/js/bse_adminui.js b/site/htdocs/js/bse_adminui.js
new file mode 100644 (file)
index 0000000..3275ef9
--- /dev/null
@@ -0,0 +1,509 @@
+
+var base_logon_fields =
+  [
+    {
+      name: "logon",
+      label: "Logon",
+      required: true
+    },
+    {
+      name: "password",
+      label: "Password",
+      required: true,
+      type: "password"
+    }
+  ];
+
+var BSEAdminUI = Class.create({
+  initialize: function() {
+    this._log = [];
+    this.api = new BSEAPI({onConfig: this._post_start.bind(this)});
+    this._messages = new BSEAdminUI.Messages("message");
+    this._modules = new Hash();
+    this._loaded_scripts = new Hash();
+    this._menubar = new BSEMenuBar({});
+  },
+  register: function (options) {
+    var mod = this._modules.get(options.name);
+    if (mod) {
+      mod.object = options.object;
+      mod.options = Object.extend(
+       Object.extend({}, this.module_defaults()),
+       options);
+    }
+    else {
+      this._log_entry("Attempt to register unknown module " + options.name);
+    }
+  },
+  module_defaults: function() {
+    return {
+      logon: true
+    };
+  },
+  add_menu: function(name, menu) {
+    var mod = this._modules.get(name);
+    if (mod) {
+      this._menubar.add_menu(menu);
+      mod.menus.push(menu);
+      if (this.current === mod) {
+       $("nav").appendChild(menu.element());
+       menu.inDocument();
+      }
+    }
+    else {
+      this._log_entry("Attempt to register menu with unknown module " + name);
+    }
+  },
+  // menu_item: function(options) {
+  //   options = Object.extend(Object.extend({}, BSEAdminUI.MenuDefaults), options);
+  //   this.handlers.set(
+  //     options.name,
+  //     { 
+  //   options: options,
+  //   key: options.name,
+  //   value: options.object,
+  //   started: false,
+  //   div: null,
+  //   suboptions: new Hash
+  //     });
+  // },
+  // submenu_item: function(options) {
+  //   options = Object.extend(Object.extend({}, BSEAdminUI.MenuDefaults), options);
+  //   this.handlers.get(options.parent).
+  //     set(options.name,
+  //     {
+  //       options: options,
+  //       key: options.name,
+  //       value: options.object,
+  //       started: false,
+  //       div: null
+  //     });
+  // },
+  //start: function() {},
+  //_bind_static_events: function() {
+  //  $("base_logon").observe("click", this._do_logoff.bindAsEventListener(this));
+  //  $("base_change_password").observe("click", this._do_changepw.bindAsEventListener(this));
+  //},
+  _post_start: function() {
+    if (this.api.conf.access_control != 0)
+      this._make_logon_menu();
+    // each line is 
+    // text;script;sortorder
+    var ui_conf = this.api.conf.admin_ui;
+    var menu_items = [];
+    for (var i in ui_conf) {
+      var entry = ui_conf[i].split(/;/);
+      menu_items.push({
+       id: "base_menu_item_" + i,
+       text: entry[0],
+       _order: entry[2],
+       onClick: this._select.bind(this, { select: i, rest: ""})
+      });
+      this._modules.set(i, {
+       title: entry[0],
+       script: entry[1],
+       name: i,
+       loaded: false,
+       div: null,
+       object: null,
+       menus: []
+      });
+    }
+    menu_items.sort(function(a, b) {
+      return a._order < b._order ? -1 : a._order > b._order ? 1 : 0;
+    });
+
+    this._menu_items = menu_items;
+    this._main_menu = new BSEMenu({
+      title: menu_items[0].text,
+      current: true,
+      items: menu_items
+    });
+    $("nav").appendChild(this._main_menu.element());
+    this._menubar.add_menu(this._main_menu);
+
+    var sel = this._parse_frag(window.location.hash);
+    this._select(sel);
+  },
+  _make_logon_menu: function() {
+    this._logon_menu = new BSEMenu({
+      title: "(none)",
+      current: true,
+      items: [
+       {
+         id: "base_menu_user_logout",
+         text: "Logoff",
+         onClick: this._do_logoff.bind(this)
+       },
+       {
+         id: "base_menu_user_changepw",
+         text: "Change password",
+         onClick: this._do_changepw.bind(this)
+       }
+      ]
+    });
+    $("nav").appendChild(this._logon_menu.element());
+    this._menubar.add_menu(this._logon_menu);
+  },
+  // _finish_load: function() {
+  //   this._log_entry("Scripts loaded, proceeding");
+  //   this._order_handlers();
+  //   this._load_menu();
+  //   var sel = this._parse_frag(window.location.hash);
+  //   this._select(sel);
+  //   $("base_wrapper").removeClassName("hide");
+  //   $("base_loading").style.display = "none";
+  // },
+  // _load_scripts: function() {
+  //   var ui_conf = this.api.conf.admin_ui;
+  //   var to_load = new Array;
+  //   for (var i in ui_conf) {
+  //     to_load.push(ui_conf[i]);
+  //   }
+  //   this._log_entry("Loading configured scripts " + to_load.join(" "));
+  //   new BSELoader({ scripts: to_load,
+  //               onLoaded: this._finish_load.bind(this) });
+  // },
+  load_css: function(css) {
+    css.each(function (e) {
+      var sty = new Element("link", { rel: "stylesheet", type: "text/css", href: e });
+      var head = $$("head")[0];
+      head.appendChild(sty);
+    });
+  },
+  // _order_handlers: function() {
+  //   // make an ordered list of the registered handlers
+  //   // first a name / object list
+  //   var list = this.handlers.values();
+  //   list.sort(
+  //     function (a, b) {
+  //   var aord = a.options.order;
+  //   var bord = b.options.order;
+  //   return aord < bord ? -1 : aord > bord ? 1 : 0;
+  //     });
+  //   this.ordered = list;
+  // },
+  // _load_menu: function() {
+  //   var menu = $("base_menu");
+  //   menu.innerHTML = "";
+  //   this.ordered.each(
+  //     function(menu, e) {
+  //   var a = new Element("a", { href: "#" + e.key, id: "base_menu_item_"+ e.key });
+  //   a.update(e.options.text);
+  //   a.observe("click", function(e, event) {
+  //     this._select({ select: e, rest: ""});
+  //     event.stop();
+  //     return false;
+  //   }.bind(this, e));
+  //   menu.appendChild(a);
+  //     }.bind(this, menu)
+  //   );
+  // },
+  // parse location (or the default "menu") to find something to display
+  _parse_frag: function(frag) {
+    if (!frag) frag = "#menu";
+    frag = frag.replace(/^\#/, '');
+    var m = /^([a-z0-9]+)(?:\/(.*))?$/.exec(frag);
+    if (m &&
+       this._modules.get(m[1]) != null) {
+      var rest = m[2] == null ? "" : m[2];
+
+      return { select: m[1], rest: rest };
+    }
+    else {
+      return { select: "menu", rest: "" };
+    }
+  },
+  // make something active, requiring a logon if the view 
+  // requires it, which most do
+  _select: function(what) {
+    var mod = this._modules.get(what.select);
+    if (mod == null) {
+      this._log_entry("attempt to select unknown " + what.select);
+      return;
+    }
+    if (!mod.loaded) {
+      var loader = new BSELoader({
+       scripts: [ mod.script ],
+       onLoaded: function(what, mod) {
+         mod.loaded = true;
+         if (mod.object) {
+           this._select(what);
+         }
+         else {
+           this._log_entry("Loaded " + what.select + " but no object registered");
+         }
+       }.bind(this, what, mod)
+      });
+    }
+    if (!mod.object)
+      return;
+    if (mod.options.logon
+       && this.api.conf.access_control != 0) {
+      if (this._userinfo) {
+       if (this._userinfo.user) {
+         this._do_select(what);
+       }
+       else {
+         this._do_logon_and_select(what);
+       }
+      }
+      else {
+       // get the user info
+       this.api.userinfo(
+         {
+           onSuccess: function(what, result) {
+             this._userinfo = result;
+             if (this._userinfo.user)
+               this._show_current_logon();
+             // try again
+             this._select(what);
+           }.bind(this, what)
+         }
+       );
+      }
+    }
+    else {
+      this._do_select(what);
+    }
+  },
+  _select_none: function() {
+    if (this.current) {
+      this.current.menus.each(function(menu) {
+       menu.element().remove();
+      });
+      this.current.object.undisplay(this, this.current.div);
+      this.current.div.style.display = "none";
+      this.current = null;
+    }
+  },
+  _do_logon_and_select: function(what) {
+    new BSEDialog({
+      onSubmit: this._on_logon_submit.bind(this, what),
+      fields: [
+       {
+         type: "fieldset",
+         legend: "Logon",
+         fields: base_logon_fields,
+       },
+      ],
+      modal: true,
+      title: "Administration",
+      submit: "Logon",
+      submit_class: "blue"
+    });
+  },
+  _on_logon_submit: function(what, dlg) {
+    this.api.logon({
+      logon: dlg.field("logon").value(),
+      password: dlg.field("password").value(),
+      onSuccess: function(what, dlg, user) {
+       this._userinfo.user = user;
+       this._show_current_logon();
+       dlg.close();
+       this._select(what);
+       this.message(user.logon + " successfully logged on");
+      }.bind(this, what, dlg),
+      onFailure: function(dlg, result) {
+       dlg.bse_error(result);
+      }.bind(this, dlg)
+    });
+  },
+  // inner make something active
+  _do_select: function(what) {
+    if (this.current)
+      this._select_none();
+    var mod = this._modules.get(what.select);
+    if (mod.started) {
+      mod.object.display(this, what.select.div, what.rest);
+      mod.div.style.display = "block";
+    }
+    else {
+      var id = mod.name.replace(/\W+/g, "-");
+      mod.div = new Element("div", { id: "app_"+id });
+      mod.object.start(this, mod.div, what.rest);
+      $("base_work").appendChild(mod.div);
+      mod.started = true;
+      this._log_entry("Started "+mod.title);
+    }
+    mod.menus.each(function(menu) {
+      $("nav").appendChild(menu.element());
+      menu.inDocument();
+    });
+    this.current = mod;
+    this._main_menu.setText(mod.title);
+  },
+  _log_entry: function(text) {
+    var now = new Date;
+    this._log.push(now.toISOString() + " " + text);
+    if (this._log.length > 1000)
+      this._log.shift();
+  },
+  _show_current_logon: function() {
+    if (this._userinfo.user) {
+      var user = this._userinfo.user;
+      if (/\S/.test(user.name))
+       this._logon_menu.setText(user.name); 
+      else
+       this._logon_menu.setText(user.logon);
+    }
+    else {
+      this._logon_menu.setText("(none)");
+    }
+  },
+  _do_logoff: function(event) {
+    //event.stop();
+    //$("base_logon").update("Logging off...");
+    this._select_none();
+    this.api.logoff({
+      onSuccess: function() {
+       this._userinfo.user = null;
+       this._show_current_logon();
+       this._select(this._parse_frag("#menu"));
+      }.bind(this),
+      onFailure: function(result) {
+       this.alert(result.msg);
+      }.bind(this)
+    });
+  },
+  _do_changepw: function(event) {
+    //event.stop();
+    if (!this._userinfo.user)
+      return;
+    new BSEDialog({
+      fields: [
+       {
+         name: "oldpassword",
+         label: "Old Password",
+         type: "password",
+         required: true
+       },
+       {
+         name: "newpassword",
+         label: "New Password",
+         type: "password",
+         required: true
+       },
+       {
+         name: "confirm",
+         label: "Confirm New Password",
+         type: "password",
+         rules: "confirm:newpassword",
+         required: true
+       }
+      ],
+      modal: true,
+      submit: "Change Password",
+      title: "Change Password",
+      cancel: true,
+      onSubmit: function(dlg) {
+       this._log_entry("Sending change password");
+       this.api.change_password({
+         oldpassword: dlg.field("oldpassword").value(),
+         newpassword: dlg.field("newpassword").value(),
+         onSuccess: function(dlg) {
+           this._log_entry("Successfully changed password");
+           dlg.close();
+           this.message("Password for " + this._userinfo.user.logon + " successfully changed");
+         }.bind(this, dlg),
+         onFailure: function(dlg, result) {
+           dlg.bse_error(result);
+         }.bind(this, dlg)
+       });
+      }.bind(this)
+    });
+  },
+  alert: function(message) {
+    new BSEDialog({
+      fields: [
+       {
+         type: "help",
+         helptext: message
+       }
+      ],
+      title: "Alert!",
+      modal: true,
+      submit: "Dismiss",
+      //submit_class: "dismiss",
+      onSubmit: function(dlg) { dlg.close(); }
+    });
+  },
+  message: function(text) {
+    this._messages.message(text);
+  },
+  busy: function() {
+  },
+  unbusy: function() {
+  }
+});
+
+var ui;
+
+var BSEUIBase = Class.create({
+  undisplay: function(ui, div) {}
+});
+
+BSEAdminUI.MenuDefaults =
+  {
+    logon: true
+  };
+
+document.observe(
+  "dom:loaded",
+  function() {
+    ui = new BSEAdminUI();
+  }
+);
+
+BSEAdminUI.Messages = Class.create({
+  initialize: function(div) {
+    this.div = $(div);
+    this.div.style.display = "none";
+  },
+  message: function(text) {
+    this.div.update(text);
+    Effect.Appear(this.div);
+    setTimeout(this._msg_done.bind(this),
+              5000);
+  },
+  _msg_done: function() {
+    Effect.Fade(this.div);
+  }
+});
+
+// var BSEContentUI = Class.create
+// (BSEUIBase,
+// {
+//   start: function(ui, div, args) {
+//     div.innerHTML = "One day I'll do something";
+//   },
+//     display: function(ui, div) {
+//     },
+//   needed_content: function(ui, args) {
+//     return { menu: "/admin/ui/menu.html" };
+//   }
+// });
+
+// document.observe("dom:loaded", function() {
+//   var handler = new BSEContentUI;
+//   ui.menu_item({
+//     name: "content",
+//     object: handler,
+//     text: "Content",
+//     order: "b"
+//   });
+//   ui.menu_item({
+//     name: "users",
+//     object: handler,
+//     text: "Users",
+//     order: "c"
+//   });
+//   ui.menu_item({
+//     name: "system",
+//     object: handler,
+//     text: "System",
+//     order: "d"
+//   });
+  
+// });
+
index a493b37..736cddc 100644 (file)
@@ -15,14 +15,18 @@ var BSEAPI = Class.create
        this.onException = function(e) {
                            alert(e);
                            };
-       this.onFailure = function(error) { alert(error.message); };
-       this._load_csrfp();
+       this.onFailure = function(error) {
+          alert(error.message);
+       };
+       this._load_csrfp(parameters);
        this.onConfig = parameters.onConfig;
-       this._load_config();
+       delete parameters.onConfig;
+       this._load_config(parameters);
      },
-     _load_csrfp: function () {
+     _load_csrfp: function (param) {
        this.get_csrfp
-       ({
+       (Object.extend
+       ({
         id: -1,
          name: this._csrfp_names,
         onSuccess: function(csrfp) {
@@ -33,11 +37,12 @@ var BSEAPI = Class.create
           // ignore this
           this._csrfp = null;
         }
-       });
+        }, param));
      },
-     _load_config: function() {
+     _load_config: function(param) {
          this.get_base_config
-         ({
+         (Object.extend
+           ({
              onSuccess:function(conf) {
                  this.conf = conf;
                  if (this.onConfig)
@@ -45,7 +50,7 @@ var BSEAPI = Class.create
              }.bind(this),
              onFailure: function(err) {
              }
-         });
+            }, param));
       },
      // logon to the server
      // logon - logon name of user
@@ -146,6 +151,38 @@ var BSEAPI = Class.create
         onException: this.onException
        });
      },
+     change_password: function(parameters) {
+       var success = parameters.onSuccess;
+       if (!success) this._badparm("change_password() missing onSuccess parameter");
+       var failure = parameters.onFailure;
+       if (!failure) failure = this.onFailure;
+       new Ajax.Request('/cgi-bin/admin/changepw.pl',
+       {
+        parameters: {
+            a_change: 1,
+            oldpassword: parameters.oldpassword,
+            newpassword: parameters.newpassword,
+            confirm: parameters.newpassword
+        },
+        onSuccess: function (success, failure, resp) {
+          if (resp.responseJSON) {
+             if(resp.responseJSON.success != 0) {
+              success();
+             }
+             else {
+              failure(this._wrap_json_failure(resp), resp);
+             }
+          }
+          else {
+            failure(this._wrap_nojson_failure(resp), resp);
+          }
+        }.bind(this, success, failure),
+        onFailure: function (failure, resp) {
+          failure(this._wrap_req_failure(resp), resp);
+        }.bind(this, failure),
+        onException: this.onException
+       });
+     },
      // fetch a tree of articles;
      // id - parent of tree to fetch
      // depth - optional depth of tree to fetch (default is large)
@@ -303,47 +340,7 @@ var BSEAPI = Class.create
      thumb_link: function(im, geoid) {
        return "/cgi-bin/thumb.pl?image="+im.id+"&g="+geoid+"&page="+im.articleId+"&f="+encodeURIComponent(im.image);
      },
-     can_drag_and_drop: function() {
-       // hopefully they're implemented at the same time
-       if (window.FormData != null)
-        return true;
-
-       if (bse_use_file_api && window.FileReader != null)
-        return true;
-
-       return false;
-     },
-     make_drop_zone: function(options) {
-       options.element.addEventListener
-       (
-        "dragenter",
-        function(options, e) {
-          e.stopPropagation();
-          e.preventDefault();
-        }.bind(this, options),
-        false
-       );
-       options.element.addEventListener
-       (
-        "dragover",
-        function(options, e) {
-          e.stopPropagation();
-          e.preventDefault();
-        }.bind(this, options),
-        false
-       );
-       options.element.addEventListener
-       (
-        "drop",
-        function(options, e) {
-          e.stopPropagation();
-          e.preventDefault();
-
-          options.onDrop(e.dataTransfer.files);
-        }.bind(this, options),
-        false
-       );
-     },
+     
      // parameters:
      //  image - file input element (required)
      //  id - owner article of the new image (required)
@@ -964,8 +961,14 @@ var BSEAPI = Class.create
      _do_request: function(url, action, other_parms, success, failure) {
        if (action != null)
          other_parms[action] = 1;
+       var async = true;
+       if (other_parms.hasOwnProperty("_async")) {
+        async = other_parms._async;
+        delete other_parms._async;
+       }
        new Ajax.Request(url,
        {
+        asynchronous: async,
         parameters: other_parms,
         onSuccess: function (success, failure, resp) {
           if (resp.responseJSON) {
@@ -1003,3 +1006,45 @@ var BSEAPI = Class.create
      _upload_id: 0
    });
 
+BSEAPI.can_drag_and_drop = function() {
+  // hopefully they're implemented at the same time
+  if (window.FormData != null)
+    return true;
+  
+  if (bse_use_file_api && window.FileReader != null)
+    return true;
+  
+  return false;
+};
+
+BSEAPI.make_drop_zone = function(options) {
+  options.element.addEventListener
+  (
+    "dragenter",
+    function(options, e) {
+      e.stopPropagation();
+      e.preventDefault();
+    }.bind(this, options),
+    false
+  );
+  options.element.addEventListener
+  (
+    "dragover",
+    function(options, e) {
+      e.stopPropagation();
+      e.preventDefault();
+    }.bind(this, options),
+    false
+  );
+  options.element.addEventListener
+  (
+    "drop",
+    function(options, e) {
+      e.stopPropagation();
+      e.preventDefault();
+      
+      options.onDrop(e.dataTransfer.files);
+    }.bind(this, options),
+    false
+  );
+};
\ No newline at end of file
diff --git a/site/htdocs/js/bse_dialog.js b/site/htdocs/js/bse_dialog.js
new file mode 100644 (file)
index 0000000..b8e0820
--- /dev/null
@@ -0,0 +1,1170 @@
+var BSEDialog = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend(
+      Object.extend({}, this.defaults()), options);
+    this._build();
+    if (this.options.dynamic_validation)
+      this._start_validation();
+    this._show();
+  },
+  _reset_errors: function() {
+    this._fields.clear_error();
+  },
+  error: function(msg) {
+    this._reset_errors();
+    this._error.update(msg);
+    this._error.show();
+    this._error_animate();
+  },
+  field_errors: function(errors) {
+    this._reset_errors();
+    if (errors.constructor == Hash) {
+      errors.each(function(entry) {
+       this._fields.set_error(entry.key, entry.value);
+      }.bind(this));
+    }
+    else {
+      for (var i in errors) {
+       this._fields.set_error(i, errors[i]);
+      }
+    }
+    this._error_animate();
+  },
+  _error_animate: function() {
+    this.options.error_animate(this, this.div);
+  },
+  field: function(name) {
+    return this._values.get(name);
+  },
+  bse_error: function(error) {
+    if (error.error_code == "FIELD") {
+      this.field_errors(error.errors);
+    }
+    else if (error.message) {
+      this.error(error.message);
+    }
+    else {
+      this.error(error_code);
+    }
+  },
+  close: function() {
+    this.div.remove();
+    if (this.wrapper)
+      this.wrapper.remove();
+    if (this._interval)
+      window.clearInterval(this._interval);
+  },
+  busy: function() {
+    this._spinner.show();
+  },
+  unbusy: function() {
+    this._spinner.hide();
+  },
+  _build: function() {
+    var top;
+    this.div = new Element("div", { className: this.options.top_class });
+    if (this.options.modal) {
+      this.wrapper = new Element("div", { className: this.options.modal_class });
+      top = this.wrapper;
+      this.wrapper.appendChild(this.div);
+    }
+    else {
+      top = this.div;
+    }
+    this.top = top;
+    this.form = new Element("form", { action: "#" });
+    this.form.observe("submit", this._on_submit.bind(this));
+    this.div.appendChild(this.form);
+
+    var parent;
+    if (this.options.fieldset_wrapper) {
+      var fs = new Element("fieldset");
+      this.title = new Element("legend");
+      this.title.update(this.options.title);
+      fs.appendChild(this.title);
+      this.form.appendChild(fs);
+      parent = fs;
+    }
+    else {
+      parent = this.form;
+      this.title = new Element("div", { className: this.options.title_class });
+      this.title.update(this.options.title);
+      this.div.appendChild(this.title);
+    }
+    this._error = new Element("div", { className: this.options.error_class });
+    this._error.hide();
+    parent.appendChild(this._error);
+    this.field_error_divs = {};
+    this.field_wrapper_divs = {};
+    this.fields = {};
+    this._add_fields(parent, this.options.fields);
+    var button_p = new Element("p", { className: this.options.submit_wrapper_class });
+    var sub_wrapper = new Element("span");
+    this._progress = new BSEDialog.ProgressBar();
+    button_p.appendChild(this._progress.element());
+    
+    this._spinner = new Element("span", {className: this.options.spinner_class });
+    this._spinner.hide();
+    this._spinner.update(this.options.spinner_text);
+    sub_wrapper.appendChild(this._spinner);
+    if (this.options.cancel) {
+      this.cancel = this._make_cancel();
+      this.cancel.observe("click", this._on_cancel.bindAsEventListener(this));
+      sub_wrapper.appendChild(this.cancel);
+    }
+    this.submit = this._make_submit();
+    sub_wrapper.appendChild(this.submit);
+    button_p.appendChild(sub_wrapper);
+    this.form.appendChild(button_p);
+  },
+  _make_cancel: function() {
+    var cancel = new Element("button", {
+      type: "button",
+      className: this.options.cancel_base_class
+    });
+    if (this.options.cancel_class)
+      cancel.addClassName(this.options.cancel_class);
+    cancel.update(this.options.cancel_text);
+
+    return cancel;
+  },
+  _make_submit: function() {
+    var submit = new Element("button", {
+      type: "submit",
+      className: this.options.submit_base_class
+    });
+    if (this.options.submit_class)
+      submit.addClassName(this.options.submit_class);
+    submit.update(this.options.submit);
+
+    return submit;
+  },
+  _show: function() {
+    var body = $$("body")[0];
+    body.insertBefore(this.div, body.firstChild);
+    if (this.wrapper)
+      body.insertBefore(this.wrapper, body.firstChild);
+    if (this.options.position) {
+      var top_px = (document.viewport.getHeight() - this.div.getHeight()) / 2;
+      if (top_px < 20) {
+       this.div.style.overflowX = "scroll";
+       this.div.style.top = "10px";
+       this.div.style.height = (this.viewport.getHeight()-20) + "px";
+      }
+      else {
+       this.div.style.top = top_px + "px";
+      }
+      this.div.style.left = (document.viewport.getWidth() - this.div.getWidth()) / 2 + "px";
+    }
+    this._fields.inDocument();
+    //if (this.wrapper) {
+    //  this.wrapper.style.height = "100%";
+    //}
+  },
+  _add_fields: function(parent, fields) {
+    this._fields = new BSEDialog.Fields(this.options);
+    this._value_fields = this._fields.value_fields();
+    this._values = new Hash();
+    this._value_fields.each(function(field) {
+      this._values.set(field.name(), field);
+    }.bind(this));
+    this._elements = this._fields.elements();
+    this._elements.each(function(parent, ele) {
+      parent.appendChild(ele);
+    }.bind(this, parent));
+  },
+  _start_validation: function() {
+    if (this.options.validator) {
+      this._update_submit();
+      this._interval = window.setInterval(this._update_submit.bind(this), this.options.dynamic_interval);
+    }
+  },
+  _update_submit: function() {
+    var errors = new Hash();
+    this.submit.disabled = this.options.validator.validate(this._values, errors) ? "" : "disabled";
+  },
+  _on_submit: function(event) {
+    event.stop();
+    if (this.options.validator) {
+      var errors = new Hash();
+      if (!this.options.validator.validate(this._values, errors)) {
+       this.field_errors(errors);
+       return;
+      }
+    }
+    this.options.onSubmit(this);
+  },
+  _on_cancel: function(event) {
+    event.stop();
+    this.options.onCancel(this);
+  },
+  defaults: function() {
+    return BSEDialog.defaults;
+  },
+  progress_start: function(note) {
+    this._progress.start(note);
+  },
+  progress: function(frac, note) {
+    this._progress.progress(frac, note);
+  },
+  progress_end: function() {
+    this._progress.end();
+  },
+  progress_note: function(note) {
+    this._progress.note(note);
+  },
+  enable: function() {
+    this.form.enable();
+  },
+  disable: function() {
+    this.form.disable();
+  }
+});
+
+BSEDialog.defaults = {
+  modal: false,
+  title: "Missing title",
+  validator: new BSEValidator,
+  top_class: "window dialog",
+  modal_class: "bse_modal",
+  title_class: "bse_title",
+  error_class: "bse_error",
+  submit_wrapper_class: "buttons",
+  submit: "Submit",
+  cancel: false,
+  cancel_text: "Cancel",
+  onCancel: function(dlg) { dlg.close(); },
+  dynamic_validation: true,
+  dynamic_interval: 1000,
+  position: false,
+  fieldset_wrapper: true,
+  cancel_base_class: "button bigrounded cancel",
+  cancel_class: "gray",
+  submit_base_class: "button bigrounded",
+  submit_class: "green",
+  error_animate: function(dlg, div) {
+    Effect.Shake(div);
+  },
+  spinner_class: "spinner",
+  spinner_text: "Busy"
+};
+
+
+// wraps one or more fields
+//
+BSEDialog.Fields = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend({}, options);
+
+    this._fields = {};
+    this._value_fields = [];
+    this._elements = [];
+    this._fieldobjs = [];
+    options.fields.each(function(field) {
+      var cls = BSEDialog.FieldTypes[field.type || "text"];
+      var fieldobj = new cls(field);
+      this._fieldobjs.push(fieldobj);
+      fieldobj.value_fields().each(function(field) {
+       this._fields[field.name()] = field;
+      }.bind(this));
+      this._value_fields = this._value_fields.concat(fieldobj.value_fields());
+      this._elements = this._elements.concat(fieldobj.elements());
+    }.bind(this));
+  },
+  set_error: function(name, message) {
+    this._fields[name].set_error(name, message);
+  },
+  clear_error: function() {
+    for (var i in this._fields) {
+      this._fields[i].clear_error();
+    }
+  },
+  value_fields: function() {
+    return this._value_fields;
+  },
+  elements: function() {
+    return this._elements;
+  },
+  inDocument: function() {
+    this._fieldobjs.each(function(f) {
+      f.inDocument();
+    });
+  }
+});
+
+BSEDialog.FieldTypes = {};
+
+// field objects provide the following methods:
+//
+// clear_error() - clear all error indicators
+// set_error(name, message) - set the error indicator for the named field
+// input() - return the underlying input tag (intended for use with file
+//      fields, many field types will not have a single input)
+// value_fields() return the fields that actually have a value.  For a field
+//   this is the child fields of the fieldset.  For value fields just return
+//   [ this ]
+// elements() returns the top-level elements for each field, for an input
+//   this is the wrapper div, for a field set, the fieldset itself
+//
+// Fields returned by value_fields() must also provide:
+// value() - return the value of the given field
+// description() - text description of the field
+// name() - name of the value
+// rules() - validation rules either as a ; separated string or an array
+// has_value() - returns true if the field has a non-empty value
+// required() - returns true if the field is marked required
+//
+BSEDialog.FieldTypes.Base = Class.create({
+  _make_wrapper: function() {
+    var wrapper = new Element("div", { className: this.options.field_wrapper_class });
+    if (this.options.required)
+      wrapper.addClassName(this.options.field_required_class);
+
+    return wrapper;
+  },
+  _make_label: function() {
+    return new Element("label", { htmlFor: this._input.identify() });
+  },
+  _make_error: function() {
+    var err_div = new Element("div", { className: this.options.field_error_class });
+    err_div.hide();
+    return err_div;
+  },
+  name: function() {
+    return this.options.name;
+  },
+  clear_error: function() {
+    this._error.update("");
+    this._error.hide();
+    this.elements().each(function(ele) {
+      ele.removeClassName(this.options.field_invalid_class);
+    }.bind(this));
+  },
+  set_error: function(name, message) {
+    this._error.update(message);
+    this._error.show();
+    this.elements().each(function(ele) {
+      ele.addClassName(this.options.field_invalid_class);
+    }.bind(this));
+  },
+  input: function() {
+    return null;
+  },
+  description: function() {
+    return null;
+  },
+  required: function() {
+    return this.options.required;
+  },
+  value_fields: function() {
+    return [ this ];
+  },
+  defaults: function() {
+    return BSEDialog.FieldTypes.Base.defaults;
+  },
+  set_object: function(object) {
+    this._object = object;
+  },
+  object: function() {
+    return this._object;
+  },
+  // called when the field becomes part of the document
+  inDocument: function() {
+  },
+  default_options: function() {
+    return BSEDialog.FieldTypes.Base.defaults;
+  }
+});
+
+BSEDialog.FieldTypes.Base.defaults = {
+  field_wrapper_class: "bse_field_wrapper",
+  field_error_class: "bse_field_error",
+  field_required_class: "bse_required",
+  field_invalid_class: "bse_invalid",
+  rules: [],
+  required: false
+};
+
+BSEDialog.FieldTypes.input = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function(options) {
+    this.options = Object.extend(Object.extend({}, this.defaults()), options);
+    this._div = this._make_wrapper();
+    var span = new Element("span");
+    this._input = this._make_input();
+    this._label = this._make_label();
+    this._label.update(this.description());
+    this._error = this._make_error();
+    
+    span.appendChild(this._input);
+    this._div.appendChild(this._label);
+    this._div.appendChild(span);
+    this._div.appendChild(this._error);
+  },
+  _make_input: function() {
+    return new Element(
+      "input",
+      {
+       name: this.options.name,
+       type: this.options.type,
+       value: this.options.value
+      });
+  },
+  value: function() {
+    return this._input.value;
+  },
+  elements: function() {
+    return [ this._div ];
+  },
+  name: function() {
+    return this.options.name;
+  },
+  description: function() {
+    return this.options.label || this.options.name;
+  },
+  rules: function() {
+    return this.options.rules;
+  },
+  has_value: function() {
+    return /\S/.test(this.value());
+  },
+  defaults: function() {
+    return Object.extend(
+      Object.extend(
+       {}, BSEDialog.FieldTypes.Base.defaults
+      ), BSEDialog.FieldTypes.input.defaults);
+  }
+});
+
+BSEDialog.FieldTypes.input.defaults = {
+  type: "text",
+  value: ""
+};
+
+BSEDialog.FieldTypes.text = BSEDialog.FieldTypes.input;
+
+BSEDialog.FieldTypes.password = BSEDialog.FieldTypes.input;
+
+BSEDialog.FieldTypes.textarea = Class.create(BSEDialog.FieldTypes.input, {
+  _make_input: function() {
+    var ta = new Element("textarea", {
+      name: this.options.name,
+      value: this.options.value,
+      cols: this.options.cols,
+      rows: this.options.rows
+    });
+    ta.update(this.options.value);
+
+    return ta;
+  },
+  defaults: function($super) {
+    return Object.extend({}, Object.extend($super(), {
+      rows: 4,
+      cols: 60
+    }));
+  }
+});
+
+BSEDialog.FieldTypes.select = Class.create(BSEDialog.FieldTypes.input, {
+  _make_input: function() {
+    var input = new Element("select", { name: this.options.name });
+    var values = this.options.values;
+    for (var i = 0; i < values.length; ++i) {
+      var val = values[i];
+      var def = this.option.value != null && this.option.value == val.key;
+      input.options[input.options.length] =
+       new Option(val.label, val.value, def);
+    }
+    return input;
+  }
+});
+
+BSEDialog.FieldTypes.radio = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function() {
+  },
+  value: function() {
+  },
+  input: function() {
+  }
+});
+
+BSEDialog.FieldTypes.fieldset = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function(options) {
+    this.options = Object.extend(Object.extend({}, BSEDialog.FieldTypes.fieldset.defaults), options);
+    this._element = new Element("fieldset");
+    if (this.options.legend) {
+      var legend = new Element("legend")
+      legend.update(this.options.legend);
+      this._element.appendChild(legend);
+    }
+    
+    this._fields = new BSEDialog.Fields(options);
+    this._fields.elements().each(function(ele) {
+      this._element.appendChild(ele);
+    }.bind(this));
+  },
+  value_fields: function() {
+    return this._fields.value_fields();
+  },
+  clear_error: function() {
+    this._fields.clear_error();
+  },
+  set_error: function(name, message) {
+    this._fields.set_error(name, message);
+  },
+  elements: function() {
+    return [ this._element ];
+  },
+  inDocument: function() {
+    this._fields.inDocument();
+  }
+});
+
+BSEDialog.FieldTypes.fieldset.defaults = {
+};
+
+BSEDialog.FieldTypes.help = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function(options) {
+    this._element = new Element("div");
+    this._element.update(options.helptext);
+  },
+  value_fields: function() {
+    // nothing to see here, move along
+    return [];
+  },
+  elements: function() {
+    return [ this._element ];
+  }
+});
+
+BSEDialog.FieldTypes.image = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function(options) {
+    this.options = Object.extend(Object.extend({}, this.default_options()), options);
+    // general container
+    var wrapper = new Element("fieldset", {
+      className: "bse_image_field"
+    });
+    this._element = wrapper;
+    var legend = new Element("legend");
+    legend.update(this.options.label);
+    wrapper.appendChild(legend);
+
+    var disp = new Element("img", {
+      className: "display"
+    });
+    this._image_display = disp;
+    this.options.value = Object.extend({
+      src: "",
+      alt: "",
+      name: "",
+      description: "",
+      display_name: ""
+    }, this.options.value || {});
+    if (this.options.value) {
+      if (this.options.value.src)
+       disp.src = this.options.value.src;
+      else if (this.options.value.file) {
+       new BSEDialog.ImagePlaceholder({
+         file: this.options.value.file,
+         onLoad: function(disp, ph) {
+           disp.src = ph.src();
+         }.bind(this, disp)
+       });
+      }
+       
+      disp.alt = this.options.value.alt;
+    }
+    
+    wrapper.appendChild(disp);
+    var file = new Element("input", {
+      type: "file"
+    });
+    this._file_input = file;
+    wrapper.appendChild(file);
+
+    if (BSEAPI.can_drag_and_drop()) {
+      BSEAPI.make_drop_zone({
+       element: disp,
+       onDrop: function (files) {
+         this.clear_error();
+         var file = files[0];
+         if (!/\.(jpe?g|png|gif)$/i.test(file.fileName)) {
+           this.set_error("Only image files accepted");
+           return;
+         }
+         this._dropped_file = file;
+         this.value.display_name = file.fileName;
+         if (window.URL && window.URL.createObjectURL) {
+           this._update_thumb_dropped(window.URL.createObjectURL(file));
+         }
+         else if (window.FileReader) {
+           var fr = new FileReader;
+           fr.onload = function(fr) {
+             this._update_thumb_dropped(fr.result);
+           }.bind(this, fr);
+           fr.readAsDataURL(file);
+         }
+
+         this._file_input.hide();
+         this._dropped_name.update(this.value.display_name);
+         this._dropped_name.show();
+       }.bind(this)
+      });
+      this._dropped_name = new Element("span", {
+       className: "dropped_name"
+      });
+      this._dropped_name.hide();
+      wrapper.appendChild(this._dropped_name);
+    }
+
+    var fields = new Array();
+    if (!this.options.hide_alt) {
+      fields.push({
+       label: "Alt",
+       type: "text",
+       name: "alt",
+       value: this.options.value.alt
+      });
+    }
+    if (!this.options.hide_name) {
+      fields.push({
+       label: "Name",
+       type: "text",
+       name: "name",
+       value: this.options.value.name
+      });
+    }
+    if (!this.options.hide_description) {
+      fields.push({
+       label: "Description",
+       type: "text",
+       name: "description",
+       value: this.options.value.description
+      });
+    }
+
+    if (fields.length != 0) {
+      var more = new Element("div", {
+       className: "more"
+      });
+      more.update("more");
+      more.observe("click", function () {
+       if (this._extras_shown)
+         this._extras.hide();
+       else
+         this._extras.show();
+       this._extras_shown = !this._extras_shown;
+      }.bind(this));
+      wrapper.appendChild(more);
+      
+      // extra image info
+      var extras = new Element("div", {
+       className: "extras"
+      });
+      this._extras = extras;
+      this._extras_shown = false;
+      this._extra_fields = new BSEDialog.Fields({
+       fields: fields
+      });
+      this._extra_fields.elements().each(function(ele) {
+       this._extras.appendChild(ele);
+      }.bind(this));
+      extras.hide();
+      wrapper.appendChild(extras);
+    }
+    this._error = this._make_error();
+    wrapper.appendChild(this._error);
+  },
+  _update_thumb_dropped: function(url) {
+    // a bit hacky
+    var img = new Element("img");
+    img.onload = function(img) {
+      var canvas = new Element("canvas", {
+       width: 80,
+       height: 80
+      });
+
+      var ctx = canvas.getContext("2d");
+      var max_dim = img.width > img.height ? img.width : img.height;
+      var scale = 80 / max_dim;
+      var sc_width = img.width * scale;
+      var sc_height = img.height * scale;
+      var off_x = (80-sc_width)/2;
+      var off_y = (80-sc_height)/2;
+      ctx.drawImage(img, off_x, off_y, 80-off_x*2, 80-off_y*2);
+      this._image_display.src = canvas.toDataURL();
+    }.bind(this, img);
+    img.src = url
+  },
+  default_options: function() {
+    return {};
+  },
+  elements: function() {
+    return [ this._element ];
+  },
+  rules: function() {
+    return [];
+  },
+  has_value: function() {
+    if (this._dropped_file)
+      return true;
+    if (this._file_input.value.length)
+      return true;
+
+    return false;
+  },
+  value: function() {
+    if (this._dropped_file)
+      return this._dropped_file.fileName;
+
+    return this._file_input.value;
+  },
+  object: function() {
+    var obj = {};
+    if (this._dropped_file) {
+      obj.file = this._dropped_file;
+      obj.display_name = obj.file.fileName;
+    }
+    else if (this._file_input.value != "") {
+      obj.file = this._file_input;
+      obj.display_name = obj.file.value;
+    }
+    if (this._extra_fields) {
+      this._extra_fields.value_fields().each(function(obj, field) {
+       obj[field.name()] = field.value();
+      }.bind(this, obj));
+    }
+
+    return obj;
+  }
+});
+
+BSEDialog.FieldTypes.gallery = Class.create(BSEDialog.FieldTypes.Base, {
+  initialize: function(options) {
+    this.options = Object.extend(
+      Object.extend({}, this.default_options()),
+      options);
+    this._element = new Element("fieldset", {
+      className: "bse_image_gallery"
+    });
+    this._undo_history = [];
+    var legend = new Element("legend");
+    legend.update(this.options.label);
+    this._element.appendChild(legend);
+    this._images_element = new Element("div", {
+      className: "bse_gallery_imagelist"
+    });
+    // original images that have been removed
+    this._deleted = [];
+    this._element.appendChild(this._images_element);
+    this._element.appendChild(this._make_input_div());
+    this._images = this.options.value.map(function(im) {
+      return {
+       type: "old",
+       image: im,
+       display_name: im.display_name,
+       id: im.id,
+       changed: false,
+       alt: im.alt,
+       description: im.description,
+       name: im.name,
+       src: im.src
+      };
+    });
+    this._autoid = 1;
+    this._populate_images();
+  },
+  _make_input_div: function() {
+    var div = new Element("div");
+    this._file_input = this._make_file_input();
+    var label = new Element("label", {
+      "for": this._file_input.identify()
+    });
+    label.update(this.options.file_input_label);
+    var add = new Element("span", {
+      className: "widget add"
+    });
+    add.update("Add");
+    add.observe("click", this._add_file_input.bind(this));
+    div.appendChild(label);
+    div.appendChild(this._file_input);
+    div.appendChild(add);
+
+    return div;
+  },
+  _make_file_input: function() {
+    var file = new Element("input", {
+      type: "file"
+    });
+
+    if (this._file_input)
+      file.id = this._file_input.identify();
+
+    if (BSEAPI.can_drag_and_drop())
+      file.multiple = true;
+
+    return file;
+  },
+  _add_file_input: function() {
+    var file = this._file_input;
+    if (file.value.length > 0) {
+      if (BSEAPI.can_drag_and_drop()) {
+       for (var i = 0; i < file.files.length; ++i) {
+         this._images.push({
+           type: "new",
+           file: file.files[i],
+           display_name: file.files[i].fileName,
+           id: "new" + this._autoid++,
+           alt: "",
+           description: "",
+           name: ""
+         });
+       }
+      }
+      else {
+       this._images.push({
+         type: "new",
+         file: file,
+         display_name: file.value,
+         id: "new" + this._autoid,
+         alt: "",
+         description: "",
+         name: ""
+       });
+       ++this._autoid;
+      }
+      this._populate_images();
+      this._make_sortable();
+
+      this._file_input = this._make_file_input();
+      file.replace(this._file_input);
+    }
+  },
+  _undo_save: function() {
+    this._undo_history.push({
+      images: this._images.clone(),
+      deleted: this._deleted.clone()
+    });
+  },
+  _undo: function() {
+    if (this._undo_history.length > 0) {
+      var entry = this._undo_history.pop();
+      this._images = entry.images;
+      this._deleted = entry.deleted;
+      this._populate_images();
+      this._make_sortable();
+    }
+  },
+  _populate_images: function() {
+    this._images_element.update();
+    this._images.each(function(im) {
+      this._images_element.appendChild(this._make_image_element(im));
+    }.bind(this));
+
+    if (BSEAPI.can_drag_and_drop()) {
+      var targ = new Element("div", {
+       className: this.options.drop_target_class
+      });
+      targ.update("Drop here");
+      BSEAPI.make_drop_zone({
+       element: targ,
+       onDrop: function(targ, files) {
+         for (var i = 0; i < files.length; ++i) {
+           this._images.push({
+             type: "new",
+             file: files[i],
+             display_name: files[i].fileName,
+             id: "new" + this._autoid,
+             alt: "",
+             description: "",
+             name: ""
+           });
+           ++this._autoid;
+           this._populate_images();
+           this._make_sortable();
+         }
+       }.bind(this, targ)
+      });
+      this._images_element.appendChild(targ);
+    }
+  },
+  _make_sortable: function() {
+    Sortable.create(this._images_element.identify(), {
+      tag: "div",
+      only: this.options.image_entry_class,
+      constraint: "horizontal",
+      overlap: "horizontal",
+      onUpdate: function() {
+      }.bind(this)
+    });
+  },
+  _make_image_element: function(im) {
+    var p = new Element("div", {
+      id: "bse_image_"+im.id,
+      className: this.options.image_entry_class
+    });
+    p.appendChild(this._make_thumb_img(im));
+    var del = new Element("span", {
+      className: "widget delete"
+    });
+    del.update("Delete");
+    del.observe("click", this._delete_image.bind(this, im));
+    p.appendChild(del);
+    var edit = new Element("span", {
+      className: "widget edit"
+    });
+    edit.update("Edit");
+    edit.observe("click", this._edit_image.bind(this, im));
+    p.appendChild(edit);
+    var namep = new Element("span", {
+      className: "name"
+    });
+    
+    namep.update(im.display_name);
+    p.appendChild(namep);
+
+    return p;
+  },
+  _make_thumb_img: function(im) {
+    if (im.thumb_img)
+      return im.thumb_img;
+
+    if (im.type == "old") {
+      if (this.options.getThumbURL) {
+       var img = new Element("img");
+       img.src = this.options.getThumbURL(im.image);
+       im.thumb_img = img;
+      }
+      else {
+       var thumb = new BSEDialog.ImagePlaceholder({
+         url: im.image.src
+       });
+       im.thumb_img = thumb.element();
+      }
+    }
+    else {
+      var thumb = new BSEDialog.ImagePlaceholder({
+       file: im.file
+      });
+      im.thumb_img = thumb.element();
+    }
+
+    return im.thumb_img;
+  },
+  _delete_image: function(im) {
+    this._undo_save();
+    if (im.type == "old")
+      this._deleted.push(im);
+    this._images = this._images.without(im);
+    this._populate_images();
+    this._make_sortable();
+  },
+  _edit_image: function(im) {
+    new BSEDialog({
+      title: "Edit Gallery Image",
+      modal: true,
+      submit: "Update",
+      cancel: true,
+      fields: [
+       {
+         name: "image",
+         type: "image",
+         value: im,
+         label: "Image"
+       }
+      ],
+      onSubmit: function(im, dlg) {
+       var result = dlg.field("image").object();
+       im.changed = true;
+       im.name = result.name;
+       im.alt = result.alt;
+       im.description = result.description;
+       if (result.file) {
+         im.file = result.file;
+         var thumb = new BSEDialog.ImagePlaceholder({
+           file: im.file
+         });
+         im.thumb_img = thumb.element();
+       }
+       this._populate_images();
+       this._make_sortable();
+       dlg.close();
+      }.bind(this, im)
+    });
+  },
+  default_options: function($super) {
+    return Object.extend(
+      Object.extend({}, $super()), {
+       value: [],
+       image_list_class: "bse_gallery_imagelist",
+       image_entry_class: "bse_gallery_image",
+       drop_target_class: "bse_drop_target",
+       file_input_label: "Add image"
+      });
+  },
+  elements: function () {
+    return [ this._element ];
+  },
+  value: function() {
+    return this._images.length > 0 ? "1" : "";
+  },
+  has_value: function() {
+    return this._value.length != 0;
+  },
+  object: function() {
+    return {
+      images: this._images,
+      deleted: this._deleted
+    };
+  },
+  rules: function() {
+    return [];
+  },
+  inDocument: function() {
+    this._make_sortable();
+  }
+});
+
+BSEDialog.AskYN = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend({
+      submit: "Yes",
+      cancel: true,
+      cancel_text: "No",
+      cancel_class: "rosy",
+      modal: true
+    }, options);
+    this.options.fields = [
+      {
+       type: "help",
+       helptext: options.text
+      }
+    ];
+    if (this.options.onYes)
+      this.options.onSubmit = this.options.onYes;
+    if (this.options.onNo)
+      this.options.onCancel = this.options.onNo;
+    var dlg = new BSEDialog(this.options);
+  }
+});
+
+BSEDialog.ProgressBar = Class.create({
+  initialize: function() {
+    this._progress = new Element("span", {
+      className: "progress"
+    });
+    this._progress.hide();
+    this._progress_status = new Element("span", {
+      className: "status"
+    });
+    this._progress.appendChild(this._progress_status);
+    this._progress_bar = new Element("span", {
+      className: "bar blue"
+    });
+    this._progress.appendChild(this._progress_bar);
+  },
+  element: function() {
+    return this._progress;
+  },
+  start: function(note) {
+    if (note != null)
+      this.note(note);
+    else
+      this._progress_status.update();
+    this._progress.show();
+    this._progress_width = this._progress.getWidth();
+    this._progress_bar.style.width = "0px";
+  },
+  progress: function(frac, note) {
+    if (frac != null) {
+      this._progress_bar.style.width = Math.floor(this._progress_width * frac) + "px";
+    }
+    if (note != null)
+      this.note(note);
+    
+  },
+  end: function() {
+    this._progress.hide();
+  },
+  note: function(note) {
+    if (note != null)
+      this._progress_status.update(note);
+    else
+      this._progress_status.update();
+  }
+});
+
+BSEDialog.ImagePlaceholder = Class.create({
+  initialize:function(options) {
+    this.options = Object.extend(this.default_options(), options);
+
+    this._element = new Element("img", {
+      width: this.options.width,
+      height: this.options.height
+    });
+
+    if (this.options.url) {
+      this._update(this.options.url);
+      return;
+    }
+
+    var file = options.file.files ? options.file.files[0] : options.file;
+    if (window.URL && window.URL.createObjectURL) {
+      this._update(window.URL.createObjectURL(file));
+    }
+    else if (window.URL && window.webkitURL.createObjectURL) {
+      this._update(window.webkitURL.createObjectURL(file));
+    }
+    else if (window.FileReader) {
+      var fr = new FileReader;
+      fr.onload = function(fr) {
+       this._update(fr.result);
+      }.bind(this, fr);
+      fr.readAsDataURL(file);
+    }
+    else {
+      this._src = this.options.noapisrc;
+      this._element.src = this._src;
+      this._onload();
+    }
+  },
+  _update: function(url) {
+    var img = new Element("img");
+    img.onload = function(img) {
+      var canvas = new Element("canvas", {
+       width: this.options.width,
+       height: this.options.height
+      });
+
+      var ctx = canvas.getContext("2d");
+      var max_dim = img.width > img.height ? img.width : img.height;
+      var scale = this.options.width / max_dim;
+      var sc_width = img.width * scale;
+      var sc_height = img.height * scale;
+      var off_x = (this.options.width - sc_width)/2;
+      var off_y = (this.options.height - sc_height)/2;
+      ctx.drawImage(img, off_x, off_y, this.options.width-off_x*2, this.options.height-off_y*2);
+      this._src = canvas.toDataURL();
+      this._element.src = this._src;
+      this._onload();
+    }.bind(this, img);
+    img.src = url
+  },
+  _onload: function() {
+    if (this.options.onLoad)
+      this.options.onLoad(this);
+  },
+  element: function() {
+    return this._element;
+  },
+  // only valid once the image is loaded
+  src: function() {
+    return this._src;
+  },
+  default_options: function() {
+    return {
+      width: 80,
+      height: 80,
+      noapisrc: "/images/ph.gif"
+    };
+  }
+});
diff --git a/site/htdocs/js/bse_loader.js b/site/htdocs/js/bse_loader.js
new file mode 100644 (file)
index 0000000..ef4f499
--- /dev/null
@@ -0,0 +1,41 @@
+var BSELoader = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend({}, options);
+    this._scripts = options.scripts.clone();
+    this._load_next_script();
+  },
+  _load_next_script: function() {
+    var uri = this._scripts.shift();
+    if (BSELoader.cache_buster) {
+      uri = uri + "?" + Math.random();
+    }
+    var scr = new Element("script", { src: uri, type: "text/javascript" });
+    scr.loadDone = false;
+    scr.onload = function(scr) {
+      if (!this.loadDone) {
+       scr.loadDone = true;
+       this._script_loaded();
+      }
+    }.bind(this, scr);
+    scr.onreadystatechange = function(scr) {
+      if ((scr.readyState === "loaded" || scr.readyState === "complete")
+         && !scr.loadDone) {
+       scr.loadDone = true;
+       this._script_loaded();
+      }
+    }.bind(this, scr);
+    var head = $$("head")[0];
+    head.appendChild(scr);
+  },
+  _script_loaded: function() {
+    if (this._scripts.length) {
+      this._load_next_script();
+    }
+    else {
+      if (this.options.onLoaded != null)
+       this.options.onLoaded();
+    }
+  }
+});
+
+BSELoader.cache_buster = false;
diff --git a/site/htdocs/js/bse_menu.js b/site/htdocs/js/bse_menu.js
new file mode 100644 (file)
index 0000000..6b99b59
--- /dev/null
@@ -0,0 +1,342 @@
+var BSEMenuBar = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend({
+      hide_timeout: 750
+    }, options);
+    this.menus = [];
+  },
+  add_menu: function(menu) {
+    this.menus.push(menu);
+    menu.element().observe("mouseover", function(ev, menu) {
+      if (this._hide_timer)
+       window.clearTimeout(this._hide_timer);
+      if (this._current_menu)  {
+       if (this._current_menu == menu) 
+         return;
+       
+       this._current_menu.submenu().hide();
+       delete this._current_menu;
+      }
+
+      this._current_menu = menu;
+      this._current_menu.submenu().show();
+    }.bindAsEventListener(this, menu));
+    menu.element().observe("mouseleave", function(ev, menu) {
+      if (this._current_menu) {
+       if (this._hide_timer)
+         window.clearTimeout(this._hide_timer);
+       this._hide_timer = window.setTimeout(function() {
+         this._current_menu.submenu().hide();
+         delete this._current_menu;
+         delete this._hide_timer;
+       }.bind(this, menu), this.options.hide_timeout);
+      }
+    }.bindAsEventListener(this, menu));
+  }
+});
+
+// represents a top-level menu item
+// 
+var BSEMenu = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend(
+      Object.extend({}, this.defaults()), options);
+    this._make_element();
+  },
+  // return an element containing the menu
+  // this must be an li
+  element: function() {
+    return this._element;
+  },
+  _make_element: function() {
+    this._element = new Element("li");
+    if (this.options.current)
+      this._element.addClassName(this.options.current_class);
+    this._title_element = new Element("span", { className: this.options.title_class });
+    this._title_element.observe("click", function(ev) { ev.stop(); });
+    this._title_element.update(this.options.title);
+    this._element.appendChild(this._title_element);
+
+    // caller provided their own content for the menu
+    this._submenu = new this.options.submenu_class(this.options);
+    this._element.appendChild(this._submenu.element());
+
+    // this._element.observe("mouseover", function(ev) {
+    //   ev.stop();
+    //   this._submenu.show();
+    //   if (this._hide_timer) {
+    //         window.clearTimeout(this._hide_timer);
+    //         delete this._hide_timer;
+    //   }
+    // }.bind(this));
+    // this._element.observe("mouseout", function(ev) {
+    //   ev.stop();
+    //   if (this._hide_timer)
+    //         window.clearTimeout(this._hide_timer);
+    //   this._hide_timer = window.setTimeout(function() {
+    //         this._submenu.hide();
+    //         delete this._hide_timer;
+    //   }.bind(this), this.options.hide_timeout);
+    // }.bind(this));
+  },
+  setText: function(text) {
+    this._title_element.update(text);
+  },
+  defaults: function() {
+    return BSEMenu.defaults;
+  },
+  inDocument: function() {
+    this._submenu.inDocument();
+  },
+  submenu: function() {
+    return this._submenu;
+  },
+  hide: function() {
+    return this._element.hide();
+  },
+  show: function() {
+    return this._element.show();
+  }
+});
+
+// an item in a drop-down menu
+BSEMenu.Item = Class.create({
+  initialize: function(options) {
+    this.original_options = options;
+    this.options = Object.extend(
+      Object.extend({}, this.defaults()), options);
+    this._make_element();
+    this.original_options.object = this;
+  },
+  element: function() {
+    return this._element;
+  },
+  submenu: function() {
+    return this._submenu;
+  },
+  setSubmenu: function(submenu) {
+    if (this._submenu) {
+      this._element.replaceChild(submenu.element(), this._submenu.element());
+    }
+    else {
+      this._element.appendChild(submenu.element());
+    }
+    this._submenu = submenu;
+  },
+  defaults: function() {
+    return BSEMenu.Item.defaults;
+  },
+  setChecked: function(checked) {
+    if (checked) {
+      this._element.removeClassName(this.options.unchecked_class);
+      this._element.addClassName(this.options.checked_class);
+    }
+    else {
+      this._element.removeClassName(this.options.checked_class);
+      this._element.addClassName(this.options.unchecked_class);
+    }
+    this._checked = checked;
+  },
+  checked: function() {
+    return this._checked;
+  },
+  setDisabled: function(disabled) {
+    if (disabled) {
+      this._element.addClassName(this.options.disabled_class);
+    }
+    else {
+      this._element.removeClassName(this.options.disabled_class);
+    }
+    this._disabled = disabled;
+  },
+  disabled: function() {
+    return this._disabled;
+  },
+  setText: function(text) {
+    this._link.update(text);
+  },
+  _make_element: function() {
+    this._element = new Element("li");
+    this._wrapper = new Element("span");
+    this._element.appendChild(this._wrapper);
+    this._link = new Element("span", { className: this.options.item_class });
+    this._link.update(this.options.text);
+    this._link.observe("click", this._onclick.bind(this));
+    this._wrapper.appendChild(this._link);
+    if (this.options.item_class)
+      this._element.addClassName(this.options.item_class);
+    if (this.options.id)
+      this._element.id = this.options.id;
+
+    this.options.widgets.each(function(w) {
+      var ele = new Element("span", { className: w.className });
+      ele.observe("click", function(event, w) {
+       event.stop();
+       w.onClick();
+      }.bindAsEventListener(this, w));
+      this._wrapper.appendChild(ele);
+    }.bind(this));
+
+    if (this.options.check)
+      this.setChecked(this.options.checked);
+    if (this.options.separate)
+      this._element.addClassName(this.options.separate_class);
+    this.setDisabled(this.options.disabled);
+
+    if (this.options.submenu) {
+      this._submenu = new this.options.submenu_class(this.options.submenu);
+      this._element.appendChild(this._submenu.element());
+    }
+  },
+  _onclick: function(ev) {
+    ev.stop();
+    if (!this._disabled && this.options.onClick)
+      this.options.onClick(this.original_options);
+  },
+  inDocument: function() {
+    if (this._submenu)
+      this._submenu.inDocument();
+  }
+});
+
+// a drop-down menu, either from the menu bar, or as a submenu of an item
+BSEMenu.SubMenu = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend(
+      Object.extend({}, this.defaults()), options);
+    this._make_element();
+  },
+  items: function() {
+    return this._items;
+  },
+  element: function() {
+    return this._element;
+  },
+  _make_element: function() {
+    if (this.options.element) {
+      this._element = this.options.element;
+    }
+    else {
+      var ele = new Element("ul");
+      this._items = [];
+      var items = this.options.items;
+      for (i = 0; i < items.length; ++i) {
+       var item = this._make_item(items[i]);
+       ele.appendChild(item.element());
+       this._items.push(item);
+      }
+      this._element = ele;
+    }
+    this._element.hide();
+  },
+  _make_item: function(options) {
+    var item = new this.options.item_class(options);
+
+    item._element.observe("mouseover", function(ev, item) {
+      if (this._shown_submenu) {
+       if (item.submenu() && item.submenu() == this._shown_submenu)
+         return;
+       this._shown_submenu.hide();
+       delete this._shown_submenu;
+      }
+      if (item.submenu()) {
+       item.submenu().show();
+       this._shown_submenu = item.submenu();
+      }
+    }.bindAsEventListener(this, item));
+
+    return item;
+  },
+  defaults: function() {
+    return BSEMenu.SubMenu.defaults;
+  },
+  inDocument: function() {
+    this._items.each(function(item) { item.inDocument() });
+  },
+  show: function() {
+    this._element.show();
+  },
+  hide: function() {
+    this._element.hide();
+    if (this._shown_submenu) {
+      this._shown_submenu.hide();
+      delete this._shown_submenu;
+    }
+  }
+});
+
+BSEMenu.OrderedSubMenu = Class.create(BSEMenu.SubMenu, {
+  //_make_element: function($super) {
+  //  $super();
+
+  //},
+  defaults: function($super) {
+    return Object.extend(
+      Object.extend({}, $super()),
+      {
+       item_class: BSEMenu.OrderedItem
+      });
+  },
+  inDocument: function($super) {
+    $super();
+
+    var item_eles = [];
+    for (var i = 0; i < this._items.length; ++i) {
+      item_eles.push(this._items[i].move_handle());
+    }
+    Sortable.create(this._element.identify(), {
+      handles: item_eles
+    });                    
+  }
+});
+
+BSEMenu.OrderedItem = Class.create(BSEMenu.Item, {
+  defaults: function($super) {
+    return Object.extend(
+      Object.extend({}, $super()),
+      {
+       move_handle_class: "move",
+       move_handle_text: "Move"
+      });
+  },
+  _make_element: function($super) {
+    $super();
+
+    var handle = new Element("span", { className: this.options.move_handle_class });
+    handle.update(this.options.move_handle_text);
+    // avoid sending a click through
+    handle.observe("click", function(ev) { ev.stop; });
+    this._wrapper.appendChild(handle);
+    this._handle = handle;
+  },
+  move_handle: function() {
+    return this._handle;
+  }
+});
+
+BSEMenu.defaults = {
+  current: false,
+  current_class: "current",
+  submenu_class: BSEMenu.SubMenu,
+  title_class: "item",
+  hide_timeout: 750
+};
+
+BSEMenu.SubMenu.defaults = {
+  items: [],
+  item_class: BSEMenu.Item
+};
+
+BSEMenu.Item.defaults = {
+  check: false,
+  separate: false,
+  disabled: false,
+  name: "",
+  checked_class: "checked",
+  unchecked_class: "unchecked",
+  separate_class: "separate",
+  disabled_class: "disabled",
+  submenu_class: BSEMenu.SubMenu,
+  item_class: "item",
+  widgets: []
+};
\ No newline at end of file
diff --git a/site/htdocs/js/bse_validate.js b/site/htdocs/js/bse_validate.js
new file mode 100644 (file)
index 0000000..6caff25
--- /dev/null
@@ -0,0 +1,204 @@
+var BSEValidator = Class.create({
+  initialize: function(options) {
+    this.options = Object.extend(
+      Object.extend({}, this.defaults()));
+  },
+  _validator: function(rule) {
+    return new BSEValidator.Rules[rule];
+  },
+  _format: function(message, field) {
+    return message.replace(/\$n/, field.description());
+  },
+  validate_one: function(field, fields, errors) {
+    if (field.required() && !field.has_value()) {
+      errors.set(field.name(), this._format(this.options.required_error, field));
+      return false;
+    }
+    var rules = field.rules();
+    var value = field.value();
+    if (typeof(rules) == "string") {
+      rules = rules.split(/;/);
+    }
+    for (var i = 0; i < rules.length; ++i) {
+      var rule = rules[i];
+      if (rule == "")
+       continue;
+      var rest = "";
+      var m = /^([0-9a-z_]+):(.*)$/.exec(rule);
+      if (m) {
+       rule = m[1];
+       rest = m[2];
+      }
+      var cls = this._validator(rule);
+      try {
+       var result = cls.test(value, this.options, rest, fields);
+       try {
+         field.set_object(result);
+       }
+       catch(e) {
+         // ignore
+       }
+      }
+      catch (e) {
+       errors.set(field.name(), e.message.replace(/\$n/, field.description()));
+       return false;
+      }
+    }
+    return true;
+  },
+  validate: function(fields, errors) {
+    fields.each(function(fields, errors, entry) {
+      this.validate_one(entry.value, fields, errors);
+    }.bind(this, fields, errors));
+
+    return errors.values().length == 0;
+  },
+  defaults: function() {
+    return BSEValidator.defaults;
+  }
+});
+
+BSEValidator.defaults = {
+  required_error: "$n is required"
+};
+
+BSEValidator.Rules = {};
+
+BSEValidator.Rules.Base = Class.create({
+});
+
+BSEValidator.Rules["date"] = Class.create(BSEValidator.Rules.Base, {
+  _days: [ 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ],
+  _days_in_month: function(year, month) {
+    if (month == 2) {
+      if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
+       return 29;
+      else
+       return 28;
+    }
+    else {
+      return this._days[month-1];
+    }
+  },
+  _parse_limit: function(limit) {
+    var m = /^([0-9]+)[^0-9]([0-9]+)[^0-9]([0-9]+)$/.exec(limit);
+    if (m) {
+      // yyyy-mm-dd
+      return new Date(parseInt(m[1]), parseInt(m[2])-1, parseInt(m[3]));
+    }
+
+    var base = new Date();
+    m = /^([+-][0-9]+)y/.match(limit);
+    if (m) {
+      base.setFullYear(base.getFullYear() + parseInt(m[1]));
+      return base;
+    }
+
+    m = /^([+-][0-9]+)y/.match(limit);
+    if (m) {
+      // adjust by ms/day
+      base.setMilliseconds(base.valueOf() + parseInt(m[1]) & 86400000);
+      return base;
+    }
+
+    throw new Error("Cannot parse date limit " + limit);
+  },
+  default_options: function() {
+    return BSEValidator.Rules["date"].defaults;
+  },
+  min_date: function() {
+    return null;
+  },
+  max_date: function() {
+    return null;
+  },
+  test: function(value, options, rest) {
+    options = Object.extend(Object.extend({}, this.default_options()), options);
+    var m = options.date_re.exec(value);
+    if (!m)
+      throw new Error(options.date_format_error);
+
+    var day, month, year;
+    var format = options.date_order.split("");
+    for (var i = 0; i < format.length; ++i) {
+      switch (format[i]) {
+      case "d":
+       day = parseInt(m[1+i]);
+       break;
+      case "m":
+       month = parseInt(m[1+i]);
+       break;
+      case "y":
+       year = parseInt(m[1+i]);
+       break;
+      }
+    }
+    if (month < 1 || month > 12)
+      throw new Error(options.date_month_range_error);
+    if (day < 1 || day > this._days_in_month(year, month))
+      throw new Error(options.date_day_range_error);
+
+    var min_date = this.min_date();
+    if (min_date != null) {
+      var min = this._parse_limit(min_date);
+      if (min.getTime() > result.getTime())
+       throw new Error(options.date_too_low_error);
+    }
+    var max_date = this.max_date();
+    if (options.max_date != null) {
+      var max = this._parse_limit(max_date);
+      if (max.getTime() < result.getTime())
+       throw new Error(options.date_too_high_error);
+    }
+    
+    var result = new Date(year, month-1, day);
+
+    return result;
+  }
+});
+
+BSEValidator.Rules["date"].defaults = {
+  date_re: /^\s*([0-9]+)[\/-]([0-9]+)[\/-]([0-9]+)\s*/,
+  date_order: "dmy",
+  date_format_error: "$n must be dd/mm/yyyy",
+  date_month_range_error: "Month out of range for $n",
+  date_day_range_error: "Day out of range for $n",
+  date_too_high_error: "$n too late",
+  date_too_low_error: "$n too early"
+};
+
+BSEValidator.Rules["future"] = Class.create(BSEValidator.Rules["date" ], {
+  min_date: function() {
+    var date = new Date();
+    date.setHours(0, 0, 0, 0);
+    return date;
+  },
+  default_options: function() {
+    return BSEValidator.Rules["future"].defaults;
+  }
+});
+
+BSEValidator.Rules["future"].defaults =
+  Object.extend({
+    date_too_low_error: "$n must be be in the future"
+  }, BSEValidator.Rules["date"].defaults);
+
+BSEValidator.Rules["confirm"] = Class.create(BSEValidator.Rules.Base, {
+  test: function(value, options, rest, fields) {
+    options = Object.extend(Object.extend({}, BSEValidator.Rules["confirm"].defaults), options);
+    var other = fields.get(rest).value();
+
+    if (value != other) {
+      var msg = options.confirm_error;
+      msg = msg.replace(/\$o/, fields.get(rest).description());
+      throw Error(msg);
+    }
+
+    return value;
+  }
+});
+
+BSEValidator.Rules["confirm"].defaults = {
+  confirm_error: "$n must be the same as $o"
+};
+
diff --git a/t-js/01validate.html b/t-js/01validate.html
new file mode 100644 (file)
index 0000000..41e08f2
--- /dev/null
@@ -0,0 +1,13 @@
+<html>
+<head>
+<title>BSEValidate tests</title>
+<script src="test.js" type="text/javascript"></script>
+<script src="../site/htdocs/js/prototype.js" type="text/javascript"></script>
+<script src="../site/htdocs/js/bse_validate.js" type="text/javascript"></script>
+<script src="01validate.js" type="text/javascript"></script>
+<link rel="stylesheet" type="text/css" href="tests.css" />
+</head>
+<body>
+<div id="tests"></div>
+</body>
+</html>
diff --git a/t-js/01validate.js b/t-js/01validate.js
new file mode 100644 (file)
index 0000000..8de45e1
--- /dev/null
@@ -0,0 +1,241 @@
+function validation_check(test, validator, checker) {
+  if (test.exception) {
+    var msg = ok_exception(function(validator, value) { 
+      validator.test(value);
+    }.bind(this, validator, test.value), "parse "+test.name, test.message);
+    if (msg) {
+      like(msg, test.message, "check message " + test.name);
+    }
+    else {
+      ok(false, test.name + " no message");
+    }
+  }
+  else {
+    var a_parsed = ok_noexception(function(validator, value){
+      return validator.test(value, {});
+    }.bind(this, validator, test.value), "parse "+test.name);
+    checker(a_parsed, test);
+  }
+}
+
+document.observe("dom:loaded", function() {
+  plan(55);
+  diag("Date validation");
+  var date_val = new BSEValidator.Rules["date"]();
+  ok(date_val, "make date validator");
+  var date_tests =
+    [
+      { 
+       name: "birthday",
+       value: "30/12/1967",
+       exception: false,
+       year: 1967,
+       month: 12,
+       day: 30
+      },
+      { 
+       name: "birthday with spaces",
+       value: " 30/12/1967 ",
+       exception: false,
+       year: 1967,
+       month: 12,
+       day: 30
+      },
+      {
+       name: "low month",
+       value: "30/0/1967",
+       exception: true,
+       message: /Month out of range/
+      },
+      {
+       name: "low month in range",
+       value: "30/1/1967",
+       exception: false,
+       year: 1967,
+       month: 1,
+       day: 30
+      },
+      {
+       name: "high month",
+       value: "30/13/1967",
+       exception: true,
+       message: /Month out of range/
+      },
+      {
+       name: "high month in range",
+       value: "30/12/1967",
+       exception: false,
+       year: 1967,
+       month: 12,
+       day: 30
+      },
+      {
+       name: "high day normal",
+       value: "32/12/1967",
+       exception: true,
+       message: /Day out of range/
+      },
+      {
+       name: "high day normal in range",
+       value: "31/12/1967",
+       exception: false,
+       year: 1967,
+       month: 12,
+       day: 31
+      },
+      {
+       name: "high day non-leap",
+       value: "29/2/1970",
+       exception: true,
+       message: /Day out of range/
+      },
+      {
+       name: "high day non-leap in range",
+       value: "28/2/1970",
+       exception: false,
+       year: 1970,
+       month: 2,
+       day: 28
+      },
+      {
+       name: "high day leap",
+       value: "30/2/1980",
+       exception: true,
+       message: /Day out of range/
+      },
+      {
+       name: "high day leap in range",
+       value: "29/2/1980",
+       exception: false,
+       year: 1980,
+       month: 2,
+       day: 29
+      },
+      {
+       name: "low day",
+       value: "0/1/1967",
+       exception: true,
+       message: /Day out of range/
+      },
+      {
+       name: "low day in range",
+       value: "1/1/1967",
+       exception: false,
+       year: 1967,
+       month: 1,
+       day: 1
+      },
+      {
+       name: "bad format",
+       value: "1/1/",
+       exception: true,
+       message: /must be dd\/mm\/yyyy$/
+      }
+    ];
+  for (var i = 0; i < date_tests.length; ++i) {
+    var test = date_tests[i];
+    validation_check(test, date_val, function(a_date, test) {
+      is(a_date.getFullYear(), test.year, test.name + " year");
+      is(a_date.getMonth(), test.month-1, test.name + " month");
+      is(a_date.getDate(), test.day, test.name + " day");
+    });
+  }
+
+  var TestField = Class.create({
+    initialize: function(options) {
+      this.options = Object.extend({ required: false, rules: [] }, options);
+    },
+    value: function() {
+      return this.options.value;
+    },
+    description: function() {
+      return this.options.description
+    },
+    name: function() {
+      return this.options.name;
+    },
+    rules: function() {
+      return this.options.rules;
+    },
+    has_value: function() {
+      return /\S/.test(this.value());
+    },
+    required: function() {
+      return this.options.required;
+    }
+  });
+
+  { // required
+    var fields = new Hash({
+      nr1: new TestField({
+       name: "nr1",
+       value: "",
+       description: "NotRequired1"
+      }),
+      nr2: new TestField({
+       name: "nr2",
+       value: " ",
+       description: "NotRequired2"
+      }),
+      r1: new TestField({
+       name: "r1",
+       value: "",
+       required: true,
+       description: "Required1"
+      }),
+      r2: new TestField({
+       name: "r2",
+       value: " ",
+       required: true,
+       description: "Required2"
+      }),
+      r3: new TestField({
+       name: "r3",
+       value: "x",
+       required: true,
+       description: "Required3"
+      })
+    });
+    var val = new BSEValidator();
+    var errors = new Hash();
+    ok(!val.validate(fields, errors), "should fail validation");
+    is(errors.get("nr1"), null, "no error for nr1");
+    is(errors.get("nr2"), null, "no error for nr2");
+    is(errors.get("r1"), "Required1 is required", "check error for r1");
+    is(errors.get("r2"), "Required2 is required", "check error for r2");
+    is(errors.get("r3"), null, "no error for r3");
+  }
+
+  {
+    // confirm
+    var fields = new Hash({
+      password: new TestField({
+       name: "password",
+       value: "abc",
+       description: "Password",
+       rules: "",
+       required: true
+      }),
+      confirm: new TestField({
+       name: "confirm",
+       value: "abc",
+       description: "Confirm",
+       rules: "confirm:password",
+      }),
+      confirm2: new TestField({
+       name: "confirm2",
+       value: "abcd",
+       description: "Confirm2",
+       rules: "confirm:password",
+      })
+    });
+    var val = new BSEValidator();
+    var errors = new Hash();
+    val.validate(fields, errors);
+    is(errors.get("confirm"), null, "should be no error for confirm");
+    is(errors.get("confirm2"), "Confirm2 must be the same as Password",
+       "confirm2 should have an error");
+  }
+
+  tests_done();
+});
\ No newline at end of file
diff --git a/t-js/10menu.html b/t-js/10menu.html
new file mode 100644 (file)
index 0000000..3a5f078
--- /dev/null
@@ -0,0 +1,15 @@
+<html>
+<head>
+<title>BSEMenu tests</title>
+<script src="test.js" type="text/javascript"></script>
+<script src="../site/htdocs/js/prototype.js" type="text/javascript"></script>
+<script src="../site/htdocs/js/bse_menu.js" type="text/javascript"></script>
+<script src="10menu.js" type="text/javascript"></script>
+<link rel="stylesheet" type="text/css" href="tests.css" />
+<link rel="stylesheet" type="text/css" href="menu.css" />
+</head>
+<body>
+<menu id="menu"><ul id="nav"></ul></menu>
+<div id="tests"></div>
+</body>
+</html>
diff --git a/t-js/10menu.js b/t-js/10menu.js
new file mode 100644 (file)
index 0000000..2280b8a
--- /dev/null
@@ -0,0 +1,145 @@
+document.observe("dom:loaded", function() {
+  plan(7);
+
+  ok(BSEMenu, "have a BSEMenu class");
+  ok(BSEMenu.Item, "have a BSEMenu.Item class");
+  ok(BSEMenu.SubMenu, "have a BSEMenu.SubMenu class");
+
+  var clicked = function(item) { diag("Item " + item.text + " clicked") };
+
+  var bar = $("nav");
+  {
+    var m1 = new BSEMenu({
+      title: "Test 1",
+      current: true,
+      items: [
+       {
+         text: "Item A",
+         separator: true,
+         onClick: clicked
+       },
+       {
+         text: "Item B",
+         onClick: clicked
+       }
+      ]
+    });
+    ok(m1, "made a menu");
+    bar.appendChild(m1.element());
+  }
+  {
+    var itemfb = {
+      text: "Item F b",
+      check: true,
+      checked: true
+    };
+    itemfb.onClick = function() {
+      var check = !this.object.checked();
+      this.object.setChecked(check);
+      diag("Item F b clicked, now " + (check ? "" : "not ") + "checked");
+    }.bind(itemfb);
+    var items = [
+       {
+         text: "Item E",
+         separate: true,
+         onClick: clicked
+       },
+       {
+         text: "Item F",
+         submenu: {
+           items: [
+             {
+               text: "Item F a",
+               disabled: true,
+               onClick: function() { diag("item F a shouldn't be clickable") }
+             },
+             itemfb
+           ]
+         }
+       }
+    ];
+    var m2 = new BSEMenu({
+      title: "Test 2",
+      items: items
+    });
+    bar.appendChild(m2.element());
+    ok(m2, "made second menu, with submenu");
+  }
+  {
+    var itemM = {
+      text: "item M",
+      onClick: clicked
+    };
+    var itemN = {
+      text: "item N",
+      onClick: clicked
+    };
+    var m3 = new BSEMenu({
+      title: "Test 3",
+      items: [
+       itemM,
+       itemN
+      ]
+    });
+    ok(m3, "made third menu");
+    bar.appendChild(m3.element());
+    m3.setText("Test 3 modified");
+    itemN.object.setText("Item N modified");
+    itemN.object.setSubmenu(new BSEMenu.SubMenu({
+      items: [
+       {
+         text: "item Na added",
+         onClick: clicked
+       },
+       {
+         text: "item Nb added",
+         onClick: clicked
+       },
+       {
+         text: "item Nc added",
+         onClick: clicked
+       },
+       {
+         text: "item Nd added",
+         onClick: clicked
+       },
+       {
+         text: "item Ne added",
+         onClick: clicked
+       }
+      ]
+    }));
+  }
+  {
+    var subT = new Element("ul");
+    var li1 = new Element("li");
+    li1.appendChild(new Element("input", { type: "text" }));
+    subT.appendChild(li1);
+    var li2 = new Element("li");
+    var a2 = new Element("a", { href: "#" });
+    a2.update("Foo");
+    li2.appendChild(a2);
+    subT.appendChild(li2);
+    
+    var itemT = {
+      text: "Articles",
+      submenu: {
+       element: subT
+      }
+    };
+    var m4 = new BSEMenu({
+      title: "Articles",
+      items: [
+       {
+         text: "New article",
+         onClick: clicked
+       },
+       itemT
+      ]
+    });
+    bar.appendChild(m4.element());
+    ok(m4, "made m4");
+  }
+  tests_done();
+});
+
diff --git a/t-js/menu.css b/t-js/menu.css
new file mode 100644 (file)
index 0000000..4001e69
--- /dev/null
@@ -0,0 +1,228 @@
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}
+
+html {
+    text-align: center;
+    width: 100%;
+}
+body {
+       width: 100%;
+    text-align: left;
+    margin: 0 auto;
+    padding: 0;
+    background-color: #666;
+    font: normal normal 0.75em/1.5 "Lucida Sans Unicode", "Lucida Grande", "Helvetica Neue", Helvetica, Arial, Helvetica, sans-serif;
+    text-shadow: 0 0.083em 0 #fff;
+    color: #585858;
+       position: relative;
+}
+
+
+#menu {
+       margin: 0;
+       padding: 0;
+       line-height: 1;
+       width: 100%;
+       height: 4em;
+       
+       -webkit-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       -moz-box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+       box-shadow: 0 0.1em 0.5em rgba(0,0,0,0.4);
+
+       background: #8b8b8b; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#a9a9a9', endColorstr='#7a7a7a'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#a9a9a9), to(#7a7a7a)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #a9a9a9, #7a7a7a); /* for firefox 3.6+ */
+
+       border-bottom: solid 0.1em #6d6d6d;
+       
+       z-index: 1000;
+       position: fixed;
+       top: 0;
+       overflow: visible;
+}
+#nav {
+       position: relative;
+       z-index: 1001;
+       overflow: visible;
+}
+#nav li {
+       margin: 0 0 0 1em;
+       padding: 0.75em 0;
+       float: left;
+       position: relative;
+       list-style: none;
+}
+#nav li li.separate {
+       border-bottom: 0.1em solid #b4b4b4;
+}
+#nav li li.separate+li {
+       border-top: 0.1em solid #fff;
+}
+/* main level link */
+#nav a {
+       position: relative;
+       line-height: 1.5;
+       font-weight: bold;
+       color: #e7e5e5;
+       text-decoration: none;
+       display: block;
+       margin: -0.1em 0;
+       padding: 0.5em 1.5em;
+       -webkit-border-radius: 1.5em;
+       -moz-border-radius: 1.5em;
+       border-radius: 1.5em;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.5);
+}
+/* main level link hover */
+#nav .current > a, #nav li:hover > a {
+       background: #d1d1d1; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ebebeb', endColorstr='#a1a1a1'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#a1a1a1)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #ebebeb, #a1a1a1); /* for firefox 3.6+ */
+
+       color: #444;
+       border-top: solid 0.1em #f8f8f8;
+       -webkit-box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       -moz-box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       box-shadow: 0 0.1em 0.1em rgba(0,0,0,0.2);
+       text-shadow: 0 0.1em 0.1em rgba(255,255,255,1);
+}
+/* sub levels link hover */
+#nav ul li:hover a, #nav li:hover li a {
+       background: none;
+       border: none;
+       color: #666;
+       -webkit-box-shadow: none;
+       -moz-box-shadow: none;
+       box-shadow: none;
+       margin: 0;
+}
+#nav ul a:hover {
+       background: #0399d4 !important; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#04acec', endColorstr='#0186ba'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#04acec), to(#0186ba)) !important; /* for webkit browsers */
+       background: -moz-linear-gradient(top, #04acec, #0186ba) !important; /* for firefox 3.6+ */
+
+       color: #fff !important;
+       -webkit-border-radius: 0;
+       -moz-border-radius: 0;
+       border-radius: 0;
+       text-shadow: 0 0.1em 0.1em rgba(0,0,0,0.1);
+}
+/* level 2 list */
+#nav ul {
+       background: #ddd; /* for non-css3 browsers */
+       filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#cfcfcf'); /* for IE */
+       background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#cfcfcf)); /* for webkit browsers */
+       background: -moz-linear-gradient(top, #fff, #cfcfcf); /* for firefox 3.6+ */
+
+       z-index: 100;
+       display: none;
+       margin: 0;
+       padding: 0;
+       width: 18em;
+       position: absolute;
+       top: 3.75em;
+       left: 0;
+       border: solid 0.1em #b4b4b4;
+       -webkit-border-radius: 0.5em;
+       -moz-border-radius: 0.5em;
+       -o-border-radius: 0.5em;
+       border-radius: 0.5em;
+       -webkit-box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+       -moz-box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+       box-shadow: 0 0.16em 0.5em rgba(0,0,0,0.3);
+}
+#nav ul ul {
+       max-height: 30.16em;
+       overflow-y: auto;
+       overflow-x: visible;
+}
+#nav ul.full {
+       width: 27em;
+}
+
+/* @group widgets */
+
+#nav li>a>span {
+       position: absolute;
+       top: 0;
+       left: 0;
+       width: 1.2em;
+       height: 100%;
+       text-indent: -9999em;
+       background: transparent url(../images/move_dk.png) no-repeat 50% 50%;
+       background-size: 1.2em;
+       display: none;
+}
+#nav li>a>span+span {
+       background-image: url(../images/delete_dk.png);
+       background-size: 1.5em;
+       width: 1.5em;
+       left: auto;
+       right: 0.5em;
+}
+#nav li:hover>a>span {
+       display: block;
+}
+
+/* @end */
+
+/* @group dropdown */
+
+#nav li:hover > ul {
+       display: block;
+}
+#nav ul li {
+       float: none;
+       margin: 0;
+       padding: 0;
+}
+#nav ul a {
+       font-weight: normal;
+       text-shadow: 0 0.1em 0.1em rgba(255,255,255,0.9);
+}
+
+/* @end */
+
+/* @group level 3+ list */
+
+#nav ul ul {
+       left: 17.5em;
+       top: -0.1em;
+}
+/*#nav ul ul.full {
+       left: 35.5em;
+}*/
+
+/* @end */
+
+/* @group rounded corners for first and last child */
+
+#nav ul li:first-child > a {
+       -webkit-border-top-left-radius: 0.5em;
+       -moz-border-radius-topleft: 0.5em;
+       border-top-left-radius: 0.5em;
+       -webkit-border-top-right-radius: 0.5em;
+       -moz-border-radius-topright: 0.5em;
+       border-top-right-radius: 0.5em;
+}
+#nav ul li:last-child > a {
+       -webkit-border-bottom-left-radius: 0.5em;
+       -moz-border-radius-bottomleft: 0.5em;
+       border-bottom-left-radius: 0.5em;
+       -webkit-border-bottom-right-radius: 0.5em;
+       -moz-border-radius-bottomright: 0.5em;
+       border-bottom-right-radius: 0.5em;
+}
+
+/* @end */
+
+/* @end */
+
+#nav input { 
+  border: none;
+  padding: 0px;
+}
+
+#tests { margin-top: 10em; }
\ No newline at end of file
diff --git a/t-js/test.js b/t-js/test.js
new file mode 100644 (file)
index 0000000..fa7f82e
--- /dev/null
@@ -0,0 +1,134 @@
+function _test_out(text, cls) {
+  var test_ele = document.getElementById('tests');
+  if (test_ele) {
+    var div_tag = document.createElement("div");
+    if (cls != null)
+      div_tag.className = cls;
+    var t = document.createTextNode(text);
+    div_tag.appendChild(t);
+    test_ele.appendChild(div_tag);
+    //var open_tag;
+    //if (cls == null)
+      //open_tag = "<div>";
+    //else
+      //open_tag = '<div class="' + cls + '">';
+    //test_ele.innerHTML = test_ele.innerHTML + open_tag + escape_html(text) + "</div>";
+  }
+}
+
+var test_ok = 0;
+var test_fails = 0;
+var test_skips = 0;
+var test_num = 1;
+var test_count;
+
+function plan(count) {
+  test_count = count;
+  _test_out("1.." + count, "plan");
+}
+
+function ok(test, comment) {
+  if (test) {
+    ++test_ok;
+    _test_out("ok " + test_num + " # " + comment, "ok");
+  }
+  else {
+    _test_out("not ok " + test_num + " # " + comment, "fail");
+    ++test_fails;
+  }
+  ++test_num;
+  return test;
+}
+
+function skip(text, count) {
+  if (count == null)
+    count = 1;
+  for (var i = 0; i < 1; ++i) {
+    _test_out("ok " + test_num + " SKIP text", "skip");
+    ++test_skips;
+    ++test_num;
+  }
+}
+
+function is(left, right, comment) {
+  var test_ok = ok(left == right, comment);
+  if (!test_ok) {
+    _test_out("# should match", "fail");
+    _test_out("# left :'"+encodeURI(left)+"'", "fail");
+    _test_out("# right:'"+encodeURI(right)+"'", "fail");
+  }
+  return test_ok;
+}
+
+function isnt(left, right, comment) {
+  var test_ok = ok(left != right, comment);
+  if (!test_ok) {
+    _test_out("# shouldn't match", "fail");
+    _test_out("# left :"+encodeURI(left), "fail");
+    _test_out("# right:"+encodeURI(right), "fail");
+  }
+  return test_ok;
+}
+
+function like(value, re, comment) {
+  var test_ok = ok(value != null && value.match(re), comment);
+  if (!test_ok) {
+    _test_out("# should match", "fail");
+    _test_out("# value:"+value, "fail");
+    _test_out("# regexp:"+re, "fail");
+  }
+  return test_ok;
+}
+
+function diag(text) {
+  _test_out("# " + text, "diag");
+}
+
+function tests_done() {
+  var cls = "ok";
+  var top_class = "passed";
+  if (test_count != null) {
+    if (test_count != test_num-1) {
+      _test_out("Expected "+test_count+" tests but saw "+(test_num-1), "fail");
+      cls = "fail";
+      top_class = "failed";
+    }
+  }
+  if (test_fails != 0) {
+    cls = "fail";
+    top_class = "failed";
+  }
+  _test_out("Summary: "+(test_num-1)+" tests, "+test_ok+" Ok, "+test_fails+" failures " + test_skips + " skips", cls);
+  var test_ele = document.getElementById('tests');
+  test_ele.className = top_class;
+}
+
+function ok_noexception(f, text) {
+  var result;
+  try {
+    result = f();
+  }
+  catch (e) {
+    ok(false, text);
+    diag(e.message);
+    return false;
+  }
+
+  ok(true, text);
+  return result;
+}
+
+function ok_exception(f, text, match) {
+  var result;
+  try {
+    f();
+  }
+  catch (e) {
+    ok(true, text);
+    diag(e.message);
+    return e.message;
+  }
+
+  ok(false, text);
+  return false;
+}
diff --git a/t-js/tests.css b/t-js/tests.css
new file mode 100644 (file)
index 0000000..98723d2
--- /dev/null
@@ -0,0 +1,24 @@
+#tests {
+  font: 10px Arial;
+}
+
+#tests.passed {
+  background-color: #CFC;
+}
+
+#tests.failed {
+  background-color: #FCC;
+}
+
+.ok {
+  color: #080;
+}
+
+.fail {
+  color: #F00;
+  font-weight: bold;
+}
+
+.skip, .plan, .diag {
+  color: #444;
+}