Rank editor is now in a working (beautiful) state. More intuitive than a Mac.
authorDan
Sat, 12 Jul 2008 03:32:57 -0400
changeset 628 ab6f55abb17e
parent 627 460e483987ab
child 629 8733c22969e7
Rank editor is now in a working (beautiful) state. More intuitive than a Mac.
includes/clientside/css/enano-shared.css
includes/clientside/static/enano-lib-basic.js
includes/clientside/static/faders.js
includes/clientside/static/functions.js
includes/clientside/static/messagebox.js
includes/clientside/static/rank-manager.js
includes/dbal.php
language/english/admin.json
plugins/admin/UserRanks.php
--- a/includes/clientside/css/enano-shared.css	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/clientside/css/enano-shared.css	Sat Jul 12 03:32:57 2008 -0400
@@ -800,6 +800,11 @@
   display: block;
   font-size: large;
   padding: 3px;
+  text-decoration: none;
+}
+
+a.rankadmin-createlink {
+  border-top: 1px solid #a0a0a0;
 }
 
 a.rankadmin-editlink:hover {
--- a/includes/clientside/static/enano-lib-basic.js	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/clientside/static/enano-lib-basic.js	Sat Jul 12 03:32:57 2008 -0400
@@ -465,7 +465,8 @@
   ajaxEditTheme: 'theme-manager.js',
   ajaxToggleSystemThemes: 'theme-manager.js',
   ajaxInstallTheme: 'theme-manager.js',
-  ajaxInitRankEdit: 'rank-manager.js'
+  ajaxInitRankEdit: 'rank-manager.js',
+  ajaxInitRankCreate: 'rank-manager.js'
 };
 
 var placeholder_instances = {};
--- a/includes/clientside/static/faders.js	Sat Jul 12 00:31:44 2008 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,821 +0,0 @@
-// Message box and visual effect system
-
-/**
- * Darkens the browser screen. This will make the entire page un-clickable except for any floating divs created after this is called. Restore with enlighten().
- * @param bool Controls whether the fade should be disabled or not. aclDisableTransitionFX will override this if set to true, and fades are never fired on IE.
- * @param int When specified, represents the numeric opacity value to set the fade layer to. 1-100.
- */
-
-var darkener_index = 0;
-
-function darken(nofade, opacVal)
-{
-  if(IE)
-    nofade = true;
-  if ( !opacVal )
-    opacVal = 70;
-  darkener_index++;
-  if(document.getElementById('specialLayer_darkener'))
-  {
-    if(nofade)
-    {
-      changeOpac(opacVal, 'specialLayer_darkener');
-      document.getElementById('specialLayer_darkener').style.display = 'block';
-      document.getElementById('specialLayer_darkener').myOpacVal = opacVal;
-    }
-    else
-    {
-      if ( document.getElementById('specialLayer_darkener').style.display != 'none' )
-      {
-        var currentOpac = document.getElementById('specialLayer_darkener').myOpacVal;
-        opacity('specialLayer_darkener', currentOpac, opacVal, 1000);
-        document.getElementById('specialLayer_darkener').myOpacVal = opacVal;
-      }
-      else
-      {
-        document.getElementById('specialLayer_darkener').style.display = 'block';
-        document.getElementById('specialLayer_darkener').myOpacVal = opacVal;
-        opacity('specialLayer_darkener', 0, opacVal, 1000);
-      }
-    }
-  } else {
-    w = getWidth();
-    h = getHeight();
-    var thediv = document.createElement('div');
-    if(IE)
-      thediv.style.position = 'absolute';
-    else
-      thediv.style.position = 'fixed';
-    if ( IE )
-    {
-      var top = getScrollOffset();
-      thediv.style.top = String(top) + 'px';
-    }
-    else
-    {
-      thediv.style.top = '0px';
-    }
-    thediv.style.left = '0px';
-    thediv.style.opacity = '0';
-    thediv.style.filter = 'alpha(opacity=0)';
-    thediv.style.backgroundColor = '#000000';
-    thediv.style.width =  '100%';
-    thediv.style.height = '100%';
-    thediv.zIndex = getHighestZ() + 5;
-    thediv.id = 'specialLayer_darkener';
-    thediv.myOpacVal = opacVal;
-    if(nofade)
-    {
-      thediv.style.opacity = ( parseFloat(opacVal) / 100 );
-      thediv.style.filter = 'alpha(opacity=' + opacVal + ')';
-      body = document.getElementsByTagName('body');
-      body = body[0];
-      body.appendChild(thediv);
-    } else {
-      body = document.getElementsByTagName('body');
-      body = body[0];
-      body.appendChild(thediv);
-      opacity('specialLayer_darkener', 0, opacVal, 1000);
-    }
-  }
-}
-
-/**
- * Un-darkens the screen and re-enables clicking of on-screen controls.
- * @param bool If true, disables the fade effect. Fades are always disabled if aclDisableTransitionFX is true and on IE.
- */
-
-function enlighten(nofade)
-{
-  if(IE)
-    nofade = true;
-  darkener_index -= 1;
-  if ( darkener_index > 0 )
-    return false;
-  if(document.getElementById('specialLayer_darkener'))
-  {
-    if(nofade)
-    {
-      document.getElementById('specialLayer_darkener').style.display = 'none';
-    }
-    else
-    {
-      var from = document.getElementById('specialLayer_darkener').myOpacVal;
-      // console.info('Fading from ' + from);
-      opacity('specialLayer_darkener', from, 0, 1000);
-      setTimeout("document.getElementById('specialLayer_darkener').style.display = 'none';", 1000);
-    }
-  }
-}
-
-/**
- * The ultimate message box framework for Javascript
- * Syntax is (almost) identical to the MessageBox command in NSIS
- * @param int type - a bitfield consisting of the MB_* constants
- * @param string title - the blue text at the top of the window
- * @param string text - HTML for the body of the message box
- * Properties:
- *   onclick - an array of functions to be called on button click events
- *             NOTE: key names are to be strings, and they must be the value of the input, CaSe-SeNsItIvE
- *   onbeforeclick - same as onclick but called before the messagebox div is destroyed
- * Methods:
- *   destroy: kills the running message box
- * Example:
- *   var my_message = new MessageBox(MB_OK|MB_ICONSTOP, 'Error logging in', 'The username and/or password is incorrect. Please check the username and retype your password');
- *   my_message.onclick['OK'] = function() {
- *       document.getElementById('password').value = '';
- *     };
- * Deps:
- *   Modern browser that supports DOM
- *   darken() and enlighten() (above)
- *   opacity() - required for darken() and enlighten()
- *   MB_* constants are defined in enano-lib-basic.js
- */
-
-var mb_current_obj;
-var mb_previously_had_darkener = false;
-
-function MessageBox(type, title, message)
-{
-  var y = getScrollOffset();
-  
-  // Prevent multiple instances
-  if ( document.getElementById('messageBox') )
-    return;
-  
-  if ( document.getElementById('specialLayer_darkener') )
-    if ( document.getElementById('specialLayer_darkener').style.display == 'block' )
-      mb_previously_had_darkener = true;
-  if ( !mb_previously_had_darkener )
-    darken(true);
-  if ( aclDisableTransitionFX )
-  {
-    document.getElementById('specialLayer_darkener').style.zIndex = '5';
-  }
-  var master_div = document.createElement('div');
-  master_div.style.zIndex = String(getHighestZ() + 5);
-  var mydiv = document.createElement('div');
-  mydiv.style.height = '200px';
-  w = getWidth();
-  h = getHeight();
-  if ( aclDisableTransitionFX )
-  {
-    master_div.style.left = ((w / 2) - 200)+'px';
-    master_div.style.top = ((h / 2) + y - 120)+'px';
-    master_div.style.position = 'absolute';
-  }
-  else
-  {
-    master_div.style.top = '-10000px';
-    master_div.style.position = ( IE ) ? 'absolute' : 'fixed';
-  }
-  z = ( aclDisableTransitionFX ) ? document.getElementById('specialLayer_darkener').style.zIndex : getHighestZ();
-  mydiv.style.backgroundColor = '#FFFFFF';
-  mydiv.style.padding = '10px';
-  mydiv.style.marginBottom = '1px';
-  mydiv.id = 'messageBox';
-  mydiv.style.overflow = 'auto';
-  
-  var buttondiv = document.createElement('div');
-  
-  mydiv.style.width = '400px';
-  buttondiv.style.width = '400px';
-  
-  w = getWidth();
-  h = getHeight();
-  if ( aclDisableTransitionFX )
-  {
-    //buttondiv.style.left = ((w / 2) - 200)+'px';
-    //buttondiv.style.top = ((h / 2) + y + 101)+'px';
-  }
-  //buttondiv.style.position = ( IE ) ? 'absolute' : 'fixed';
-  z = ( aclDisableTransitionFX ) ? document.getElementById('specialLayer_darkener').style.zIndex : getHighestZ();
-  buttondiv.style.backgroundColor = '#C0C0C0';
-  buttondiv.style.padding = '10px';
-  buttondiv.style.textAlign = 'right';
-  buttondiv.style.verticalAlign = 'middle';
-  buttondiv.id = 'messageBoxButtons';
-  
-  this.clickHandler = function() { messagebox_click(this, mb_current_obj); };
-  
-  if( ( type & MB_ICONINFORMATION || type & MB_ICONSTOP || type & MB_ICONQUESTION || type & MB_ICONEXCLAMATION ) && !(type & MB_ICONLOCK) )
-  {
-    mydiv.style.paddingLeft = '50px';
-    mydiv.style.width = '360px';
-    mydiv.style.backgroundRepeat = 'no-repeat';
-    mydiv.style.backgroundPosition = '8px 8px';
-  }
-  else if ( type & MB_ICONLOCK )
-  {
-    mydiv.style.paddingLeft = '50px';
-    mydiv.style.width = '360px';
-    mydiv.style.backgroundRepeat = 'no-repeat';
-  }
-  
-  if(type & MB_ICONINFORMATION)
-  {
-    mydiv.style.backgroundImage = 'url(\''+scriptPath+'/images/info.png\')';
-  }
-  
-  if(type & MB_ICONQUESTION)
-  {
-    mydiv.style.backgroundImage = 'url(\''+scriptPath+'/images/question.png\')';
-  }
-  
-  if(type & MB_ICONSTOP)
-  {
-    mydiv.style.backgroundImage = 'url(\''+scriptPath+'/images/error.png\')';
-  }
-  
-  if(type & MB_ICONEXCLAMATION)
-  {
-    mydiv.style.backgroundImage = 'url(\''+scriptPath+'/images/warning.png\')';
-  }
-  
-  if(type & MB_ICONLOCK)
-  {
-    mydiv.style.backgroundImage = 'url(\''+scriptPath+'/images/lock.png\')';
-  }
-  
-  if(type & MB_OK)
-  {
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_ok');
-    btn._GenericName = 'OK';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-  }
-  
-  if(type & MB_OKCANCEL)
-  {
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_ok');
-    btn._GenericName = 'OK';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-    
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_cancel');
-    btn._GenericName = 'Cancel';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-  }
-  
-  if(type & MB_YESNO)
-  {
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_yes');
-    btn._GenericName = 'Yes';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-    
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_no');
-    btn._GenericName = 'No';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-  }
-  
-  if(type & MB_YESNOCANCEL)
-  {
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_yes');
-    btn._GenericName = 'Yes';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-    
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_no');
-    btn._GenericName = 'No';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-    
-    btn = document.createElement('input');
-    btn.type = 'button';
-    btn.value = $lang.get('etc_cancel');
-    btn._GenericName = 'Cancel';
-    btn.onclick = this.clickHandler;
-    btn.style.margin = '0 3px';
-    buttondiv.appendChild(btn);
-  }
-  
-  heading = document.createElement('h2');
-  heading.innerHTML = title;
-  heading.style.color = '#50A0D0';
-  heading.style.fontFamily = 'trebuchet ms, verdana, arial, helvetica, sans-serif';
-  heading.style.fontSize = '12pt';
-  heading.style.fontWeight = 'lighter';
-  heading.style.textTransform = 'lowercase';
-  heading.style.marginTop = '0';
-  mydiv.appendChild(heading);
-  
-  var text = document.createElement('div');
-  text.innerHTML = String(message);
-  this.text_area = text;
-  mydiv.appendChild(text);
-  
-  this.updateContent = function(text)
-    {
-      this.text_area.innerHTML = text;
-    };
-    
-  this.destroy = function()
-    {
-      var mbdiv = document.getElementById('messageBox');
-      mbdiv.parentNode.removeChild(mbdiv.nextSibling);
-      mbdiv.parentNode.removeChild(mbdiv);
-      if ( !mb_previously_had_darkener )
-        enlighten(true);
-    };
-  
-  //domObjChangeOpac(0, mydiv);
-  //domObjChangeOpac(0, master_div);
-  
-  body = document.getElementsByTagName('body');
-  body = body[0];
-  master_div.appendChild(mydiv);
-  master_div.appendChild(buttondiv);
-  
-  body.appendChild(master_div);
-  
-  if ( !aclDisableTransitionFX )
-    setTimeout('mb_runFlyIn();', 100);
-  
-  this.onclick = new Array();
-  this.onbeforeclick = new Array();
-  mb_current_obj = this;
-}
-
-var messagebox = MessageBox;
-
-function mb_runFlyIn()
-{
-  var mydiv = document.getElementById('messageBox');
-  var maindiv = mydiv.parentNode;
-  fly_in_top(maindiv, true, false);
-}
-
-function messagebox_click(obj, mb)
-{
-  val = ( typeof ( obj._GenericName ) == 'string' ) ? obj._GenericName : obj.value;
-  if(typeof mb.onbeforeclick[val] == 'function')
-  {
-    var o = mb.onbeforeclick[val];
-    var resp = o();
-    if ( resp )
-      return false;
-    o = false;
-  }
-  
-  var mydiv = document.getElementById('messageBox');
-  var maindiv = mydiv.parentNode;
-  
-  if ( aclDisableTransitionFX )
-  {
-    var mbdiv = document.getElementById('messageBox');
-    mbdiv.parentNode.removeChild(mbdiv.nextSibling);
-    mbdiv.parentNode.removeChild(mbdiv);
-    if ( !mb_previously_had_darkener )
-      enlighten(true);
-  }
-  else
-  {
-    var to = fly_out_top(maindiv, true, false);
-    setTimeout("var mbdiv = document.getElementById('messageBox'); mbdiv.parentNode.removeChild(mbdiv.nextSibling); mbdiv.parentNode.removeChild(mbdiv); if ( !mb_previously_had_darkener ) enlighten(true);", to);
-  }
-  if(typeof mb.onclick[val] == 'function')
-  {
-    o = mb.onclick[val];
-    o();
-    o = false;
-  }
-}
-
-function testMessageBox()
-{
-  mb = new MessageBox(MB_OKCANCEL|MB_ICONINFORMATION, 'Javascripted dynamic message boxes', 'This is soooooo coool, now if only document.createElement() worked in IE!<br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text<br /><br /><br /><br /><br />this is some more text');
-  mb.onclick['OK'] = function()
-    {
-      alert('You clicked OK!');
-    }
-  mb.onbeforeclick['Cancel'] = function()
-    {
-      alert('You clicked Cancel!');
-    }
-}
-
-/**
- * The miniPrompt function, for creating small prompts and dialogs. The window will be flown in and the window darkened with opac=0.4.
- * @param function Will be passed an HTMLElement that is the body of the prompt window; the function can do with this as it pleases
- */
-
-function miniPrompt(call_on_create)
-{
-  if ( document.getElementById('specialLayer_darkener') )
-  {
-    if ( document.getElementById('specialLayer_darkener').style.display != 'none' )
-    {
-      var opac = parseFloat(document.getElementById('specialLayer_darkener').style.opacity);
-      opac = opac * 100;
-      darken(aclDisableTransitionFX, opac);
-    }
-    else
-    {
-      darken(aclDisableTransitionFX, 40);
-    }
-  }
-  else
-  {
-    darken(aclDisableTransitionFX, 40);
-  }
-  
-  var wrapper = document.createElement('div');
-  wrapper.className = 'miniprompt';
-  var top = document.createElement('div');
-  top.className = 'mp-top';
-  var body = document.createElement('div');
-  body.className = 'mp-body';
-  var bottom = document.createElement('div');
-  bottom.className = 'mp-bottom';
-  if ( typeof(call_on_create) == 'function' )
-  {
-    call_on_create(body);
-  }
-  wrapper.appendChild(top);
-  wrapper.appendChild(body);
-  wrapper.appendChild(bottom);
-  var left = ( getWidth() / 2 ) - ( 388 / 2 );
-  wrapper.style.left = left + 'px';
-  var top = getScrollOffset() - 27;
-  wrapper.style.top = top + 'px';
-  domObjChangeOpac(0, wrapper);
-  var realbody = document.getElementsByTagName('body')[0];
-  realbody.appendChild(wrapper);
-  
-  if ( aclDisableTransitionFX )
-  {
-    domObjChangeOpac(100, wrapper);
-  }
-  else
-  {
-    fly_in_top(wrapper, true, true);
-    
-    setTimeout(function()
-      {
-        domObjChangeOpac(100, wrapper);
-      }, 40);
-  }
-}
-
-/**
- * For a given element, loops through the element and all of its ancestors looking for a miniPrompt div, and returns it. Returns false on failure.
- * @param object:HTMLElement Child node to scan
- * @return object
- */
-
-function miniPromptGetParent(obj)
-{
-  while ( true )
-  {
-    // prevent infinite loops
-    if ( !obj || obj.tagName == 'BODY' )
-      return false;
-    
-    if ( $dynano(obj).hasClass('miniprompt') )
-    {
-      return obj;
-    }
-    obj = obj.parentNode;
-  }
-  return false;
-}
-
-/**
- * Destroys the first miniPrompt div encountered by recursively checking all parent nodes.
- * Usage: <a href="javascript:miniPromptDestroy(this);">click</a>
- * @param object:HTMLElement a child of the div.miniprompt
- * @param bool If true, does not call enlighten().
- */
-
-function miniPromptDestroy(obj, nofade)
-{
-  obj = miniPromptGetParent(obj);
-  if ( !obj )
-    return false;
-  
-  // found it
-  var parent = obj.parentNode;
-  if ( !nofade )
-    enlighten(aclDisableTransitionFX);
-  if ( aclDisableTransitionFX )
-  {
-    parent.removeChild(obj);
-  }
-  else
-  {
-    var timeout = fly_out_top(obj, true, true);
-    setTimeout(function()
-      {
-        parent.removeChild(obj);
-      }, timeout);
-  }
-}
-
-/**
- * Simple test case
- */
-
-function miniPromptTest()
-{
-  miniPrompt(function(div) { div.innerHTML = 'hello world! <a href="#" onclick="miniPromptDestroy(this); return false;">destroy me</a>'; });
-}
-
-/**
- * Message box system for miniPrompts. Less customization but easier to scale than the regular messageBox framework.
- * @example
- <code>
- miniPromptMessage({
-   title: 'Delete page',
-   message: 'Do you really want to delete this page? This is reversible unless you clear the page logs.',
-   buttons: [
-     {
-       text: 'Delete',
-       color: 'red',
-       style: {
-         fontWeight: 'bold'
-       },
-       onclick: function() {
-         ajaxDeletePage();
-         miniPromptDestroy(this);
-       }
-     },
-     {
-       text: 'cancel',
-       onclick: function() {
-         miniPromptDestroy(this);
-       }
-     }
-   ]
- });
- </code>
- */
-
-function miniPromptMessage(parms)
-{
-  if ( !parms.title || !parms.message || !parms.buttons )
-    return false;
-  
-  return miniPrompt(function(parent)
-    {
-      try
-      {
-        var h3 = document.createElement('h3');
-        h3.appendChild(document.createTextNode(parms.title));
-        var body = document.createElement('p');
-        var message = parms.message.split(unescape('%0A'));
-        for ( var i = 0; i < message.length; i++ )
-        {
-          body.appendChild(document.createTextNode(message[i]));
-          if ( i + 1 < message.length )
-            body.appendChild(document.createElement('br'));
-        }
-        
-        parent.style.textAlign = 'center';
-        
-        parent.appendChild(h3);
-        parent.appendChild(body);
-        parent.appendChild(document.createElement('br'));
-        
-        // construct buttons
-        for ( var i = 0; i < parms.buttons.length; i++ )
-        {
-          var button = parms.buttons[i];
-          button.input = document.createElement('a');
-          button.input.href = '#';
-          button.input.clickAction = button.onclick;
-          button.input.className = 'abutton';
-          if ( button.color )
-          {
-            button.input.className += ' abutton_' + button.color;
-          }
-          button.input.appendChild(document.createTextNode(button.text));
-          if ( button.style )
-          {
-            for ( var j in button.style )
-            {
-              button.input.style[j] = button.style[j];
-            }
-          }
-          button.input.onclick = function(e)
-          {
-            try
-            {
-              this.clickAction(e);
-            }
-            catch(e)
-            {
-              console.error(e);
-            }
-            return false;
-          }
-          parent.appendChild(button.input);
-        }
-        if ( parms.buttons[0] )
-        {
-          setTimeout(function()
-            {
-              parms.buttons[0].input.focus();
-            }, 300);
-        }
-      }
-      catch ( e )
-      {
-        console.error(e);
-      }
-    });
-}
-
-function testMPMessageBox()
-{
-  miniPromptMessage({
-    title: 'The Game of LIFE question #73',
-    message: 'You just got your girlfriend pregnant. Please select an option:',
-    buttons: [
-      {
-        text: 'Abort',
-        color: 'red',
-        style: {
-          fontWeight: 'bold'
-        },
-        onclick: function() {
-          miniPromptDestroy(this);
-        }
-      },
-      {
-        text: 'Retry',
-        color: 'blue',
-        onclick: function() {
-          miniPromptDestroy(this);
-        }
-      },
-      {
-        text: 'Ignore',
-        color: 'green',
-        onclick: function() {
-          miniPromptDestroy(this);
-        }
-      }
-    ]
-  });
-}
-
-// Function to fade classes info-box, warning-box, error-box, etc.
-
-function fadeInfoBoxes()
-{
-  var divs = new Array();
-  d = document.getElementsByTagName('div');
-  j = 0;
-  for(var i in d)
-  {
-    if ( !d[i] )
-      continue;
-    if ( !d[i].tagName )
-      continue;
-    if(d[i].className=='info-box' || d[i].className=='error-box' || d[i].className=='warning-box' || d[i].className=='question-box')
-    {
-      divs[j] = d[i];
-      j++;
-    }
-  }
-  if(divs.length < 1) return;
-  for(i in divs)
-  {
-    if(!divs[i].id) divs[i].id = 'autofade_'+Math.floor(Math.random() * 100000);
-    switch(divs[i].className)
-    {
-      case 'info-box':
-      default:
-        from = '#3333FF';
-        break;
-      case 'error-box':
-        from = '#FF3333';
-        break;
-      case 'warning-box':
-        from = '#FFFF33';
-        break;
-      case 'question-box':
-        from = '#33FF33';
-        break;
-    }
-    Fat.fade_element(divs[i].id,30,2000,from,Fat.get_bgcolor(divs[i].id));
-  }
-}
-
-// Alpha fades
-
-function opacity(id, opacStart, opacEnd, millisec) {
-    //speed for each frame
-    var speed = Math.round(millisec / 100);
-    var timer = 0;
-
-    //determine the direction for the blending, if start and end are the same nothing happens
-    if(opacStart > opacEnd) {
-        for(i = opacStart; i >= opacEnd; i--) {
-            setTimeout("changeOpac(" + i + ",'" + id + "')",(timer * speed));
-            timer++;
-        }
-    } else if(opacStart < opacEnd) {
-        for(i = opacStart; i <= opacEnd; i++)
-            {
-            setTimeout("changeOpac(" + i + ",'" + id + "')",(timer * speed));
-            timer++;
-        }
-    }
-}
-
-var opacityDOMCache = new Object();
-function domOpacity(obj, opacStart, opacEnd, millisec) {
-    //speed for each frame
-    var speed = Math.round(millisec / 100);
-    var timer = 0;
-    
-    // unique ID for this animation
-    var uniqid = Math.floor(Math.random() * 1000000);
-    opacityDOMCache[uniqid] = obj;
-
-    //determine the direction for the blending, if start and end are the same nothing happens
-    if(opacStart > opacEnd) {
-        for(i = opacStart; i >= opacEnd; i--) {
-            setTimeout("var obj = opacityDOMCache["+uniqid+"]; domObjChangeOpac(" + i + ",obj)",(timer * speed));
-            timer++;
-        }
-    } else if(opacStart < opacEnd) {
-        for(i = opacStart; i <= opacEnd; i++)
-            {
-            setTimeout("var obj = opacityDOMCache["+uniqid+"]; domObjChangeOpac(" + i + ",obj)",(timer * speed));
-            timer++;
-        }
-    }
-    setTimeout("delete(opacityDOMCache["+uniqid+"]);",(timer * speed));
-}
-
-//change the opacity for different browsers
-function changeOpac(opacity, id) {
-    var object = document.getElementById(id).style;
-    object.opacity = (opacity / 100);
-    object.MozOpacity = (opacity / 100);
-    object.KhtmlOpacity = (opacity / 100);
-    object.filter = "alpha(opacity=" + opacity + ")";
-}
-
-function mb_logout()
-{
-  var mb = new MessageBox(MB_YESNO|MB_ICONQUESTION, $lang.get('user_logout_confirm_title'), $lang.get('user_logout_confirm_body'));
-  mb.onclick['Yes'] = function()
-    {
-      window.location = makeUrlNS('Special', 'Logout/' + csrf_token + '/' + title);
-    }
-}
-
-function whiteOutElement(el)
-{
-  var top = $(el).Top();
-  var left = $(el).Left();
-  var width = $(el).Width();
-  var height = $(el).Height();
-  
-  var blackout = document.createElement('div');
-  blackout.style.position = 'absolute';
-  blackout.style.top = top + 'px';
-  blackout.style.left = left + 'px';
-  blackout.style.width = width + 'px';
-  blackout.style.height = height + 'px';
-  
-  blackout.style.backgroundColor = '#FFFFFF';
-  domObjChangeOpac(60, blackout);
-  blackout.style.backgroundImage = 'url(' + scriptPath + '/includes/clientside/tinymce/themes/advanced/skins/default/img/progress.gif)';
-  blackout.style.backgroundPosition = 'center center';
-  blackout.style.backgroundRepeat = 'no-repeat';
-  blackout.style.zIndex = getHighestZ() + 2;
-  
-  var body = document.getElementsByTagName('body')[0];
-  body.appendChild(blackout);
-  
-  return blackout;
-}
-
--- a/includes/clientside/static/functions.js	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/clientside/static/functions.js	Sat Jul 12 03:32:57 2008 -0400
@@ -439,6 +439,26 @@
   return blackout;
 }
 
+/**
+ * Take a div generated by whiteOutElement() and report success using the glossy "check" graphic. Sets the image, then
+ * briefly fades in, then fades out and destroys the box so as to re-allow control over the underlying element
+ */
+
+function whiteOutReportSuccess(whitey)
+{
+  // fade the status indicator in and then out
+  whitey.style.backgroundImage = 'url(' + scriptPath + '/images/check.png)';
+  domOpacity(whitey, 60, 80, 500);
+  setTimeout(function()
+    {
+      domOpacity(whitey, 60, 0, 500);
+    }, 750);
+  setTimeout(function()
+    {
+      whitey.parentNode.removeChild(whitey);
+    }, 1250);
+}
+
 // other DHTML functions
 
 function fetch_offset(obj)
--- a/includes/clientside/static/messagebox.js	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/clientside/static/messagebox.js	Sat Jul 12 03:32:57 2008 -0400
@@ -380,6 +380,8 @@
         domObjChangeOpac(100, wrapper);
       }, 40);
   }
+  
+  return wrapper;
 }
 
 /**
@@ -477,28 +479,36 @@
 
 function miniPromptMessage(parms)
 {
-  if ( !parms.title || !parms.message || !parms.buttons )
+  if ( ( !parms.title && !parms.message ) || !parms.buttons )
     return false;
   
   return miniPrompt(function(parent)
     {
       try
       {
-        var h3 = document.createElement('h3');
-        h3.appendChild(document.createTextNode(parms.title));
-        var body = document.createElement('p');
-        var message = parms.message.split(unescape('%0A'));
-        for ( var i = 0; i < message.length; i++ )
+        if ( parms.title )
+        {
+          var h3 = document.createElement('h3');
+          h3.appendChild(document.createTextNode(parms.title));
+        }
+        if ( parms.message )
         {
-          body.appendChild(document.createTextNode(message[i]));
-          if ( i + 1 < message.length )
-            body.appendChild(document.createElement('br'));
+          var body = document.createElement('p');
+          var message = parms.message.split(unescape('%0A'));
+          for ( var i = 0; i < message.length; i++ )
+          {
+            body.appendChild(document.createTextNode(message[i]));
+            if ( i + 1 < message.length )
+              body.appendChild(document.createElement('br'));
+          }
         }
         
         parent.style.textAlign = 'center';
         
-        parent.appendChild(h3);
-        parent.appendChild(body);
+        if ( parms.title )
+          parent.appendChild(h3);
+        if ( parms.message )
+          parent.appendChild(body);
         parent.appendChild(document.createElement('br'));
         
         // construct buttons
--- a/includes/clientside/static/rank-manager.js	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/clientside/static/rank-manager.js	Sat Jul 12 03:32:57 2008 -0400
@@ -4,7 +4,7 @@
 
 var RankEditorControl = function(rankdata)
 {
-  this.rankdata = rankdata;
+  this.rankdata = ( typeof(rankdata) == 'object' ) ? rankdata : {};
   
   // have the browser parse CSS for us and use an anchor to be as close
   // as possible in calculating CSS
@@ -25,6 +25,8 @@
     editor.className = 'tblholder';
     // stash this editor instance in the parent div for later function calls
     editor.editor = this;
+    this.wrapperdiv = editor;
+    editor.style.width = '100%';
     
     // tables suck.
     var table = document.createElement('table');
@@ -48,6 +50,7 @@
     }
     th_head.appendChild(document.createTextNode($lang.get(th_head_string, th_head_data)));
     tr_head.appendChild(th_head);
+    this.th_head = th_head;
     table.appendChild(tr_head);
     
     // row: rank title
@@ -64,6 +67,11 @@
     f_rank_title.type = 'text';
     f_rank_title.size = '30';
     f_rank_title.value = ( this.editing ) ? this.rankdata.rank_title : '';
+    f_rank_title.editor = this;
+    f_rank_title.onkeyup = function()
+    {
+      this.editor.renderPreview();
+    }
     this.f_rank_title = f_rank_title;
     td_title_f.appendChild(f_rank_title);
     
@@ -90,6 +98,7 @@
     f_basic_bold.onclick = function()
     {
       this.editor.style_sim_obj.style.fontWeight = ( this.checked ) ? 'bold' : null;
+      this.editor.renderPreview();
     }
     l_basic_bold.style.fontWeight = 'bold';
     l_basic_bold.appendChild(f_basic_bold);
@@ -105,6 +114,7 @@
     f_basic_italic.onclick = function()
     {
       this.editor.style_sim_obj.style.fontStyle = ( this.checked ) ? 'italic' : null;
+      this.editor.renderPreview();
     }
     l_basic_italic.style.fontStyle = 'italic';
     l_basic_italic.appendChild(f_basic_italic);
@@ -120,6 +130,7 @@
     f_basic_underline.onclick = function()
     {
       this.editor.style_sim_obj.style.textDecoration = ( this.checked ) ? 'underline' : null;
+      this.editor.renderPreview();
     }
     l_basic_underline.style.textDecoration = 'underline';
     l_basic_underline.appendChild(f_basic_underline);
@@ -152,6 +163,7 @@
     f_rank_color.size = '7';
     f_rank_color.value = ( this.editing ) ? this.rgb2hex(this.style_sim_obj.style.color) : '';
     f_rank_color.style.backgroundColor = this.style_sim_obj.style.color;
+    f_rank_color.editor = this;
     this.f_rank_color = f_rank_color;
     f_rank_color.onkeyup = function(e)
     {
@@ -168,6 +180,15 @@
       if ( this.value.length == 6 || this.value.length == 3 )
       {
         this.style.backgroundColor = '#' + this.value;
+        this.editor.style_sim_obj.style.color = '#' + this.value;
+        this.style.color = '#' + this.editor.determineLightness(this.value);
+        this.editor.renderPreview();
+      }
+      else if ( this.value.length == 0 )
+      {
+        this.style.backgroundColor = null;
+        this.editor.style_sim_obj.style.color = null;
+        this.editor.renderPreview();
       }
     }
     td_color_f.appendChild(f_rank_color);
@@ -176,6 +197,64 @@
     tr_color.appendChild(td_color_f);
     table.appendChild(tr_color);
     
+    // "field": preview
+    var tr_preview = document.createElement('tr');
+    var td_preview_l = document.createElement('td');
+    td_preview_l.className = 'row2';
+    td_preview_l.appendChild(document.createTextNode($lang.get('acpur_field_preview')));
+    tr_preview.appendChild(td_preview_l);
+    
+    var td_preview_f = document.createElement('td');
+    td_preview_f.className = 'row2';
+    var div_preview = document.createElement('a');
+    this.preview_div = div_preview;
+    div_preview.style.fontSize = 'x-large';
+    div_preview.appendChild(document.createTextNode(''));
+    div_preview.firstChild.nodeValue = ( this.editing ) ? this.rankdata.rank_title : '';
+    td_preview_f.appendChild(div_preview);
+    tr_preview.appendChild(td_preview_f);
+    
+    table.appendChild(tr_preview);
+    
+    // submit button
+    var tr_submit = document.createElement('tr');
+    var th_submit = document.createElement('th');
+    th_submit.className = 'subhead';
+    th_submit.setAttribute('colspan', '2');
+    var btn_submit = document.createElement('input');
+    btn_submit.type = 'submit';
+    btn_submit.value = ( this.editing ) ? $lang.get('acpur_btn_save') : $lang.get('acpur_btn_create_submit');
+    btn_submit.editor = this;
+    btn_submit.style.fontWeight = 'bold';
+    btn_submit.onclick = function(e)
+    {
+      this.editor.submitEvent(e);
+    }
+    this.btn_submit = btn_submit;
+    th_submit.appendChild(btn_submit);
+    
+    // delete button
+    if ( this.editing )
+    {
+      var btn_delete = document.createElement('input');
+      btn_delete.type = 'button';
+      btn_delete.value = $lang.get('acpur_btn_delete');
+      btn_delete.editor = this;
+      btn_delete.onclick = function(e)
+      {
+        this.editor.deleteEvent(e);
+      }
+      th_submit.appendChild(document.createTextNode(' '));
+      th_submit.appendChild(btn_delete);
+    }
+    
+    tr_submit.appendChild(th_submit);
+    
+    table.appendChild(tr_submit);
+    
+    // render preview
+    this.renderPreview();
+    
     // finalize the editor table
     editor.appendChild(table);
     
@@ -186,6 +265,62 @@
     return editor;
   }
   
+  /**
+   * Takes the existing editor div and transforms the necessary elements so that it goes from "create" mode to "edit" mode
+   * @param object Edit data - same format as the rankdata parameter to the constructor, but we should only need rank_id
+   */
+  
+  this.transformToEditor = function(rankdata)
+  {
+    // we need a rank ID
+    if ( typeof(rankdata.rank_id) != 'number' )
+      return false;
+    
+    if ( this.editing )
+      return false;
+    
+    this.editing = true;
+    
+    this.rankdata = rankdata;
+    this.rankdata.rank_title = this.f_rank_title.value;
+    this.rankdata.rank_style = this.getCSS();
+    
+    // transform various controls
+    this.th_head.firstChild.nodeValue = $lang.get('acpur_th_edit_rank', {
+        rank_title: $lang.get(this.rankdata.rank_title)
+      });
+    this.btn_submit.value = $lang.get('acpur_btn_save');
+    
+    // add the delete button
+    var th_submit = this.btn_submit.parentNode;
+    
+    var btn_delete = document.createElement('input');
+    btn_delete.type = 'button';
+    btn_delete.value = $lang.get('acpur_btn_delete');
+    btn_delete.editor = this;
+    btn_delete.onclick = function(e)
+    {
+      this.editor.deleteEvent(e);
+    }
+    th_submit.appendChild(document.createTextNode(' '));
+    th_submit.appendChild(btn_delete);
+    
+    return true;
+  }
+  
+  /**
+   * Takes a hex color, averages the three channels, and returns either 'ffffff' or '000000' depending on the luminosity of the color.
+   * @param string
+   * @return string
+   */
+  
+  this.determineLightness = function(hexval)
+  {
+    var rgb = this.hex2rgb(hexval);
+    var lumin = ( rgb[0] + rgb[1] + rgb[2] ) / 3;
+    return ( lumin > 60 ) ? '000000' : 'ffffff';
+  }
+  
   this.getJSONDataset = function()
   {
     
@@ -193,7 +328,43 @@
   
   this.getCSS = function()
   {
-    
+    return this.style_sim_obj.getAttribute('style');
+  }
+  
+  this.renderPreview = function()
+  {
+    if ( !this.preview_div )
+      return false;
+    var color = ( this.style_sim_obj.style.color ) ? '#' + this.rgb2hex(this.style_sim_obj.style.color) : null;
+    this.preview_div.style.color = color;
+    this.preview_div.style.fontWeight = this.style_sim_obj.style.fontWeight;
+    this.preview_div.style.fontStyle = this.style_sim_obj.style.fontStyle;
+    this.preview_div.style.textDecoration = this.style_sim_obj.style.textDecoration;
+    this.preview_div.firstChild.nodeValue = $lang.get(this.f_rank_title.value);
+  }
+  
+  this.submitEvent = function(e)
+  {
+    if ( this.onsubmit )
+    {
+      this.onsubmit(e);
+    }
+    else
+    {
+      window.console.error('RankEditorControl: no onsubmit event specified');
+    }
+  }
+  
+  this.deleteEvent = function(e)
+  {
+    if ( this.ondelete )
+    {
+      this.ondelete(e);
+    }
+    else
+    {
+      window.console.error('RankEditorControl: no ondelete event specified');
+    }
   }
   
   /**
@@ -218,6 +389,31 @@
     
     return r + g + b;
   }
+  
+  /**
+   * Get red, green, and blue values for the given hex color
+   * @param string
+   * @return array (numbered, e.g. not an object
+   */
+  
+  this.hex2rgb = function(hex)
+  {
+    hex = hex.replace(/^#/, '');
+    if ( hex.length != 3 && hex.length != 6 )
+    {
+      return hex;
+    }
+    if ( hex.length == 3 )
+    {
+      // is there a better way to do this?
+      hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
+    }
+    hex = [ hex.substr(0, 2), hex.substr(2, 2), hex.substr(4, 2) ];
+    var red = parseInt(hex[0], 16);
+    var green = parseInt(hex[1], 16);
+    var blue = parseInt(hex[2], 16);
+    return [red, green, blue];
+  }
 }
 
 /**
@@ -226,9 +422,274 @@
 
 function ajaxInitRankEdit(rank_id)
 {
-  var editor = new RankEditorControl({ rank_title: 'Foo', rank_id: rank_id, rank_style: 'color: #ff0000; font-weight: bold;' });
-  var ren
+  load_component('messagebox');
+  var json_packet = {
+    mode: 'get_rank',
+    rank_id: rank_id
+  };
+  json_packet = ajaxEscape(toJSONString(json_packet));
+  ajaxPost(makeUrlNS('Admin', 'UserRanks/action.json'), 'r=' + json_packet, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        var response = String(ajax.responseText + '');
+        if ( response.substr(0, 1) != '{' )
+        {
+          handle_invalid_json(ajax.responseText);
+          return false;
+        }
+        try
+        {
+          var response = parseJSON(ajax.responseText);
+        }
+        catch(e)
+        {
+          handle_invalid_json(ajax.responseText);
+        }
+        var editor = new RankEditorControl(response);
+        editor.onsubmit = ajaxRankEditHandleSaveExisting;
+        editor.ondelete = ajaxRankEditHandleDelete;
+        var container = document.getElementById('admin_ranks_container_right');
+        container.innerHTML = '';
+        container.appendChild(editor.render());
+      }
+    }, true);
+}
+
+function ajaxInitRankCreate()
+{
+  load_component('messagebox');
+  var editor = new RankEditorControl();
+  editor.onsubmit = ajaxRankEditHandleSaveNew;
   var container = document.getElementById('admin_ranks_container_right');
   container.innerHTML = '';
   container.appendChild(editor.render());
 }
+
+function ajaxRankEditHandleSave(editor, switch_new)
+{
+  var whitey = whiteOutElement(editor.wrapperdiv);
+  
+  // pack it up, ...
+  var json_packet = {
+    mode: ( switch_new ) ? 'create_rank' : 'save_rank',
+    rank_title: editor.f_rank_title.value,
+    rank_style: editor.getCSS()
+  }
+  if ( !switch_new )
+  {
+    json_packet.rank_id = editor.rankdata.rank_id;
+  }
+  /// ... pack it in
+  var json_packet = ajaxEscape(toJSONString(json_packet));
+  
+  ajaxPost(makeUrlNS('Admin', 'UserRanks/action.json'), 'r=' + json_packet, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        var response = String(ajax.responseText + '');
+        if ( response.substr(0, 1) != '{' )
+        {
+          handle_invalid_json(ajax.responseText);
+          return false;
+        }
+        try
+        {
+          var response = parseJSON(ajax.responseText);
+        }
+        catch(e)
+        {
+          handle_invalid_json(ajax.responseText);
+        }
+        if ( response.mode == 'success' )
+        {
+          whiteOutReportSuccess(whitey);
+          if ( switch_new )
+          {
+            //
+            // we have a few more things to do with a newly created rank.
+            //
+            
+            // 1. transform editor
+            editor.transformToEditor(response);
+            editor.onsubmit = ajaxRankEditHandleSaveExisting;
+            editor.ondelete = ajaxRankEditHandleDelete;
+            
+            // 2. append the new rank to the list
+            var create_link = document.getElementById('rankadmin_createlink');
+            if ( create_link )
+            {
+              var parent = create_link.parentNode;
+              var edit_link = document.createElement('a');
+              edit_link.href = '#rank_edit:' + response.rank_id;
+              edit_link.className = 'rankadmin-editlink';
+              edit_link.setAttribute('style', editor.getCSS());
+              edit_link.id = 'rankadmin_editlink_' + response.rank_id;
+              edit_link.rank_id = response.rank_id;
+              edit_link.appendChild(document.createTextNode($lang.get(editor.f_rank_title.value)));
+              parent.insertBefore(edit_link, create_link);
+              edit_link.onclick = function()
+              {
+                ajaxInitRankEdit(this.rank_id);
+              }
+            }
+          }
+          else
+          {
+            // update the rank title on the left
+            var edit_link = document.getElementById('rankadmin_editlink_' + editor.rankdata.rank_id);
+            if ( edit_link )
+            {
+              edit_link.firstChild.nodeValue = $lang.get(editor.f_rank_title.value);
+              edit_link.setAttribute('style', editor.getCSS());
+            }
+          }
+        }
+        else
+        {
+          whitey.parentNode.removeChild(whitey);
+          miniPromptMessage({
+              title: $lang.get('acpur_err_save_failed_title'),
+              message: response.error,
+              buttons: [
+                {
+                  text: $lang.get('etc_ok'),
+                  color: 'red',
+                  style: {
+                    fontWeight: 'bold'
+                  },
+                  onclick: function()
+                  {
+                    miniPromptDestroy(this);
+                  }
+                }
+              ]
+          });
+        }
+      }
+    }, true);
+}
+
+var ajaxRankEditHandleSaveExisting = function()
+{
+  ajaxRankEditHandleSave(this, false);
+}
+
+var ajaxRankEditHandleSaveNew = function()
+{
+  ajaxRankEditHandleSave(this, true);
+}
+
+var ajaxRankEditHandleDelete = function()
+{
+  var mp = miniPromptMessage({
+      title: $lang.get('acpur_msg_rank_delete_confirm_title'),
+      message: $lang.get('acpur_msg_rank_delete_confirm_body'),
+      buttons: [
+        {
+          text: $lang.get('acpur_btn_delete'),
+          color: 'red',
+          style: {
+            fontWeight: 'bold'
+          },
+          onclick: function()
+          {
+            var parent = miniPromptGetParent(this);
+            var editor = parent.editor;
+            setTimeout(function()
+              {
+                ajaxRankEditDeleteConfirmed(editor);
+              }, 1000);
+            miniPromptDestroy(parent);
+          }
+        },
+        {
+          text: $lang.get('etc_cancel'),
+          onclick: function()
+          {
+            miniPromptDestroy(this);
+          }
+        }
+      ]
+    });
+  console.debug(mp);
+  mp.editor = this;
+}
+
+function ajaxRankEditDeleteConfirmed(editor)
+{
+  var whitey = whiteOutElement(editor.wrapperdiv);
+  
+  load_component('SpryEffects');
+  
+  var json_packet = {
+    mode: 'delete_rank',
+    rank_id: editor.rankdata.rank_id
+  };
+  var rank_id = editor.rankdata.rank_id;
+  
+  json_packet = ajaxEscape(toJSONString(json_packet));
+  ajaxPost(makeUrlNS('Admin', 'UserRanks/action.json'), 'r=' + json_packet, function()
+    {
+      if ( ajax.readyState == 4 && ajax.status == 200 )
+      {
+        var response = String(ajax.responseText + '');
+        if ( response.substr(0, 1) != '{' )
+        {
+          handle_invalid_json(ajax.responseText);
+          return false;
+        }
+        try
+        {
+          var response = parseJSON(ajax.responseText);
+        }
+        catch(e)
+        {
+          handle_invalid_json(ajax.responseText);
+        }
+        if ( response.mode == 'success' )
+        {
+          // the deletion was successful, report success and kill off the editor
+          whiteOutReportSuccess(whitey);
+          setTimeout(function()
+            {
+              // nuke the rank title on the left
+              var edit_link = document.getElementById('rankadmin_editlink_' + editor.rankdata.rank_id);
+              if ( edit_link )
+              {
+                edit_link.parentNode.removeChild(edit_link);
+              }
+              // collapse and destroy the editor
+              new Spry.Effect.Blind(editor.wrapperdiv, { duration: 500, finish: function()
+                  {
+                    // when the animation finishes, nuke the whole thing
+                    var container = document.getElementById('admin_ranks_container_right');
+                    container.innerHTML = $lang.get('acpur_msg_select_rank');
+                  }
+                }).start();
+            }, 1500);
+        }
+        else
+        {
+          whitey.parentNode.removeChild(whitey);
+          miniPromptMessage({
+              title: $lang.get('acpur_err_delete_failed_title'),
+              message: response.error,
+              buttons: [
+                {
+                  text: $lang.get('etc_ok'),
+                  color: 'red',
+                  style: {
+                    fontWeight: 'bold'
+                  },
+                  onclick: function()
+                  {
+                    miniPromptDestroy(this);
+                  }
+                }
+              ]
+          });
+        }
+      }
+    }, true);
+}
--- a/includes/dbal.php	Sat Jul 12 00:31:44 2008 -0400
+++ b/includes/dbal.php	Sat Jul 12 03:32:57 2008 -0400
@@ -1104,7 +1104,56 @@
   
   function insert_id()
   {
-    return @pg_last_oid();
+    // list of primary keys in Enano tables
+    // this is a bit hackish, but not much choice.
+    static $primary_keys = false;
+    if ( !is_array($primary_keys) )
+    {
+      $primary_keys = array(
+        table_prefix . 'comments' => 'comment_id',
+        table_prefix . 'logs' => 'log_id',
+        table_prefix . 'users' => 'user_id',
+        table_prefix . 'banlist' => 'ban_id',
+        table_prefix . 'files' => 'file_id',
+        table_prefix . 'buddies' => 'buddy_id',
+        table_prefix . 'privmsgs' => 'message_id',
+        table_prefix . 'sidebar' => 'item_id',
+        table_prefix . 'hits' => 'hit_id',
+        table_prefix . 'groups' => 'group_id',
+        table_prefix . 'group_members' => 'member_id',
+        table_prefix . 'acl' => 'rule_id',
+        table_prefix . 'page_groups' => 'pg_id',
+        table_prefix . 'page_group_members' => 'pg_member_id',
+        table_prefix . 'tags' => 'tag_id',
+        table_prefix . 'lockout' => 'id',
+        table_prefix . 'language' => 'lang_id',
+        table_prefix . 'language_strings' => 'string_id',
+        table_prefix . 'ranks' => 'rank_id',
+        table_prefix . 'captcha' => 'code_id',
+        table_prefix . 'diffiehellman' => 'key_id',
+        table_prefix . 'plugins' => 'plugin_id'
+      );
+      // allow plugins to patch this if needed
+      global $plugins;
+      $code = $plugins->setHook('pgsql_set_serial_list');
+      foreach ( $code as $cmd )
+      {
+        eval($cmd);
+      }
+    }
+    $last_was_insert = preg_match('/^INSERT INTO ([a-z0-9_]+)\(/i', $this->latest_query, $match);
+    if ( $last_was_insert )
+    {
+      // trick based on PunBB's PostgreSQL driver
+      $table =& $match[1];
+      if ( isset($primary_keys[$table]) )
+      {
+        $primary_key = "{$table}_{$primary_keys[$table]}_seq";
+        $q = pg_query("SELECT CURRVAL('$primary_key');");
+        return ( $q ) ? intval(@pg_fetch_result($q, 0)) : false;
+      }
+    }
+    return false;
   }
   
   function fetchrow($r = false) {
--- a/language/english/admin.json	Sat Jul 12 00:31:44 2008 -0400
+++ b/language/english/admin.json	Sat Jul 12 03:32:57 2008 -0400
@@ -923,6 +923,8 @@
       intro_para4: 'This screen lets you create, edit, and delete ranks. To assign them, edit the respective user or usergroup.',
       
       msg_select_rank: 'To edit a rank, select it on the left. Or, click Create New Rank to start a new rank.',
+      msg_rank_delete_confirm_title: 'Delete this rank?',
+      msg_rank_delete_confirm_body: 'This will completely remove this rank and unassociate any users and groups with it.',
       
       th_edit_rank: 'Edit rank: %rank_title%',
       th_create_rank: 'Create new rank',
@@ -933,6 +935,17 @@
       field_style_basic_italic: 'Italic',
       field_style_basic_underline: 'Underline',
       field_style_color: 'Username color:',
+      field_preview: 'Preview:',
+      
+      btn_save: 'Save rank',
+      btn_create_submit: 'Create rank',
+      btn_create_init: 'Create new rank',
+      btn_delete: 'Delete rank',
+      
+      err_save_failed_title: 'Saving operation failed',
+      err_delete_failed_title: 'Deletion operation failed',
+      err_missing_rank_title: 'Please enter a title for this rank.',
+      err_cant_delete_system_rank: 'The rank you are trying to delete is an important system rank that cannot be deleted. However, you may customize the title and style of it to make it appear normal.',
     },
     acpsl: {
       heading_main: 'System security log',
--- a/plugins/admin/UserRanks.php	Sat Jul 12 00:31:44 2008 -0400
+++ b/plugins/admin/UserRanks.php	Sat Jul 12 03:32:57 2008 -0400
@@ -34,7 +34,176 @@
   
   if ( $paths->getParam(0) == 'action.json' )
   {
-    // ajax call
+    // ajax call, try to decode json request
+    header('Content-type: application/json');
+    
+    if ( !isset($_POST['r']) )
+    {
+      echo enano_json_encode(array(
+          'mode' => 'error',
+          'error' => 'Missing JSON request payload'
+        ));
+      return true;
+    }
+    try
+    {
+      $request = enano_json_decode($_POST['r']);
+    }
+    catch ( Exception $e )
+    {
+      echo enano_json_encode(array(
+          'mode' => 'error',
+          'error' => 'Invalid JSON request payload'
+        ));
+      return true;
+    }
+    
+    if ( !isset($request['mode']) )
+    {
+      echo enano_json_encode(array(
+          'mode' => 'error',
+          'error' => 'JSON request payload does not contain required parameter "mode"'
+        ));
+      return true;
+    }
+    
+    // we've got it
+    switch ( $request['mode'] )
+    {
+      case 'get_rank':
+        // easy enough, get a rank from the DB
+        $rank_id = intval(@$request['rank_id']);
+        if ( empty($rank_id) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => 'Missing rank ID'
+            ));
+          return true;
+        }
+        // query and fetch
+        $q = $db->sql_query('SELECT rank_id, rank_title, rank_style FROM ' . table_prefix . "ranks WHERE rank_id = $rank_id;");
+        if ( !$q || $db->numrows() < 1 )
+          $db->die_json();
+        
+        $row = $db->fetchrow();
+        $db->free_result();
+        
+        // why does mysql do this?
+        $row['rank_id'] = intval($row['rank_id']);
+        echo enano_json_encode($row);
+        break;
+      case 'save_rank':
+        // easy enough, get a rank from the DB
+        $rank_id = intval(@$request['rank_id']);
+        // note - an empty rank_style field is permitted
+        if ( empty($rank_id) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => 'Missing rank ID'
+            ));
+          return true;
+        }
+        
+        if ( empty($request['rank_title']) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => $lang->get('acpur_err_missing_rank_title')
+            ));
+          return true;
+        }
+        
+        // perform update
+        $rank_title = $db->escape($request['rank_title']);
+        $rank_style = $db->escape(@$request['rank_style']);
+        $q = $db->sql_query('UPDATE ' . table_prefix . "ranks SET rank_title = '$rank_title', rank_style = '$rank_style' WHERE rank_id = $rank_id;");
+        
+        echo enano_json_encode(array(
+            'mode' => 'success'
+          ));
+        break;
+      case 'create_rank':
+        if ( empty($request['rank_title']) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => $lang->get('acpur_err_missing_rank_title')
+            ));
+          return true;
+        }
+        
+        $rank_title = $db->escape($request['rank_title']);
+        $rank_style = $db->escape(@$request['rank_style']);
+        
+        // perform insert
+        $q = $db->sql_query('INSERT INTO ' . table_prefix . "ranks ( rank_title, rank_style ) VALUES\n"
+                          . "  ( '$rank_title', '$rank_style' );");
+        if ( !$q )
+          $db->die_json();
+        
+        $rank_id = $db->insert_id();
+        if ( !$rank_id )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => 'Refetch of rank ID failed'
+            ));
+          return true;
+        }
+        
+        echo enano_json_encode(array(
+            'mode' => 'success',
+            'rank_id' => $rank_id
+          ));
+        break;
+      case 'delete_rank':
+        // nuke a rank
+        $rank_id = intval(@$request['rank_id']);
+        if ( empty($rank_id) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => 'Missing rank ID'
+            ));
+          return true;
+        }
+        
+        // is this rank protected (e.g. a system rank)?
+        if ( in_array($rank_id, $protected_ranks) )
+        {
+          echo enano_json_encode(array(
+              'mode' => 'error',
+              'error' => $lang->get('acpur_err_cant_delete_system_rank')
+            ));
+          return true;
+        }
+        
+        // unset any user and groups that might be using it
+        $q = $db->sql_query('UPDATE ' . table_prefix . "users SET user_rank = NULL WHERE user_rank = $rank_id;");
+        if ( !$q )
+          $db->die_json();
+        $q = $db->sql_query('UPDATE ' . table_prefix . "groups SET group_rank = NULL WHERE group_rank = $rank_id;");
+        if ( !$q )
+          $db->die_json();
+        
+        // now remove the rank itself
+        $q = $db->sql_query('DELETE FROM ' . table_prefix . "ranks WHERE rank_id = $rank_id;");
+        if ( !$q )
+          $db->_die();
+        
+        echo enano_json_encode(array(
+            'mode' => 'success'
+          ));
+        break;
+      default:
+        echo enano_json_encode(array(
+          'mode' => 'error',
+          'error' => 'Unknown requested operation'
+        ));
+      return true;
+    }
     return true;
   }
   
@@ -60,8 +229,9 @@
     // a string that isn't in the category_stringid format
     $rank_title = $lang->get($row['rank_title']);
     // FIXME: make sure htmlspecialchars() is escaping quotes and backslashes
-    echo '<a href="#rank_edit:' . $row['rank_id'] . '" onclick="ajaxInitRankEdit(' . $row['rank_id'] . '); return false;" class="rankadmin-editlink" style="' . htmlspecialchars($row['rank_style']) . '">' . htmlspecialchars($rank_title) . '</a> ';
+    echo '<a href="#rank_edit:' . $row['rank_id'] . '" onclick="ajaxInitRankEdit(' . $row['rank_id'] . '); return false;" class="rankadmin-editlink" style="' . htmlspecialchars($row['rank_style']) . '" id="rankadmin_editlink_' . $row['rank_id'] . '">' . htmlspecialchars($rank_title) . '</a> ';
   }
+  echo '<a href="#rank_create" onclick="ajaxInitRankCreate(); return false;" class="rankadmin-editlink rankadmin-createlink" id="rankadmin_createlink">' . $lang->get('acpur_btn_create_init') . '</a> ';
   echo '</div>';
   
   echo '<div class="rankadmin-right" id="admin_ranks_container_right">';