// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

// for those w/o console...  sad.
if (typeof(console) == 'undefined') {
  var Console = Class.create({
    initialize: function() {
      this._logs = [];
      this._timers = {};
      var host = window.location.host;
      if (host.match(/localhost|\.local|10\.0\.2\.2/))
        document.observe('keydown', this.dumpLog.bindAsEventListener(this));
    },

    dumpLog: function(e) {
      // ctrl-` dumps the log in an alert
      if (e.ctrlKey && e.keyCode == 192) {
        alert(this._logs.join("\n"));
        this._logs = [];
      }
    },

    log: function(obj) {
      this._logs.push(obj);
    },

    error: function(obj) {
      this.log(obj);
    },

    warn: function(obj) {
      this.log(obj);
    },

    time: function(name) {
      this._timers[name] = new Date();
    },

    timeEnd: function(name) {
      var ms = new Date() - this._timers[name];
      this.log(name + ': ' + ms + 'ms');

      this._timers[name] = null;
    }
  });

  console = new Console();
}

// Setup scriptaculous defaults
if (typeof(Effect) != "undefined" && Effect.DefaultOptions) {
  // speed up animations
  Effect.DefaultOptions.duration = 0.2;
}

// strftime from http://redhanded.hobix.com/inspect/showingPerfectTime.html
/* other support functions -- thanks, ecmanaut! */
(function() {
  var strftime_funks = {
    local: false,
    quarter: function( m, q ){ return q + (Math.floor(m/3) + 1); },
    zeropad: function( n, nz ){ return (nz || n>9) ? n : '0'+n; },
    a: function(t)    { return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][(this.local ? t.getDay() : t.getUTCDay())]; },
    A: function(t)    { return ['Sunday','Monday','Tuedsay','Wednesday','Thursday','Friday','Saturday'][(this.local ? t.getDay() : t.getUTCDay())]; },
    b: function(t)    { return ['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'][(this.local ? t.getMonth() : t.getUTCMonth())]; },
    B: function(t)    { return ['January','February','March','April','May','June', 'July','August',
        'September','October','November','December'][(this.local ? t.getMonth() : t.getUTCMonth())]; },
    c: function(t)    { return t.toString(); },
    d: function(t,nz) { return this.zeropad((this.local ? t.getDate() : t.getUTCDate()), nz); },
    e: function(t)    { return (this.local ? t.getDate() : t.getUTCDate()); },
    H: function(t,nz) { return this.zeropad((this.local ? t.getHours() : t.getUTCHours()), nz); },
    I: function(t,nz) { return this.zeropad(this.l(t), nz); },
    l: function(t)    { return 12 - (24 - (this.local ? t.getHours() : t.getUTCHours())) % 12; },
    m: function(t,nz) { return this.zeropad((this.local ? t.getMonth() : t.getUTCMonth())+1, nz); }, // month-1
    M: function(t,nz) { return this.zeropad((this.local ? t.getMinutes() : t.getUTCMinutes()), nz); },
    p: function(t)    { return this.H(t) < 12 ? 'AM' : 'PM'; },
    P: function(t)    { return this.H(t) < 12 ? 'am' : 'pm'; },
    q: function(t)    { return this.quarter((this.local ? t.getMonth() : t.getUTCMonth()), "Q"); },
    Q: function(t)    { return this.quarter((this.local ? t.getMonth() : t.getUTCMonth()), "Quarter "); },
    S: function(t,nz) { return this.zeropad((this.local ? t.getSeconds() : t.getUTCSeconds()), nz); },
    w: function(t)    { return (this.local ? t.getDay() : t.getUTCDay()); }, // 0..6 == sun..sat
    y: function(t,nz) { return this.zeropad(this.Y(t) % 100, nz); },
    Y: function(t)    { return (this.local ? t.getFullYear() : t.getUTCFullYear()); },
    '%': function(t)  { return '%'; }
  };

  // standardize the times that timeplot uses
  Object.extend(Date.prototype, {
    strftime: function(fmt, local) {
      var t = this;
      for (var s in strftime_funks) {
        strftime_funks.local = local;
          if (s.length == 1 ) {
            fmt = fmt.replace('%' + s, strftime_funks[s](t));
            fmt = fmt.replace('%-1' + s, strftime_funks[s](t, true));
          }
      }
      return fmt;
    },

    toLocaleTimeString: function() {
      return this.strftime('%l:%M%p');
    },

    toLocaleDateString: function() {
      return this.strftime('%b %e, %Y');
    }

  });

  Element.addMethods({
    enableTextSelection: function(element, enable) {
      if (enable === false) {
        if (Prototype.Browser.IE || Prototype.Browser.WebKit) {
          element._old = element.onselectstart;
          element.onselectstart = function(){return false;};
        } else if (Prototype.Browser.Gecko) {
          element._old = element.getStyle('MozUserSelect');
          element.setStyle({MozUserSelect: "none"});
        } else {
          element._old = this._div.onmousedown;
          element.onmousedown = function(){return false;};
        }
      } else {
        if (Prototype.Browser.IE || Prototype.Browser.WebKit) {
          element.onselectstart = element._oldOnselectstart;
          delete element._old;
        } else if (Prototype.Browser.Gecko) {
          element.setStyle({MozUserSelect: element._oldMozUserSelect});
          delete element._old;
        } else {
          this._div.onmousedown = element._oldOnmousedown;
          delete element._old;
        }
      }
    }
  });

  Object.extend(String.prototype, {
    toDate: function() {
      try {
        var dateTime = this.gsub(/[TZ]/, ' ').split(/ /);
        var date = dateTime[0].split('-');
        return (new Date(date[1]+'/'+date[2]+'/'+date[0]+' '+dateTime[1]));
      } catch(e) {
        return(false);
      }
    }
  });

  Date.prototype.toLocaleString = function(separator) {
    if (!this.localeString) {
      this.localeString = this.toLocaleDateString()
      if (this.getHours() != 0 || this.getMinutes() != 0) {
        separator = separator || ' ';
        this.localeString += separator + this.toLocaleTimeString();
      }
    }

    return this.localeString;
  };
})();

// a wonky binary search (wonky = it could use some generalizing)
Object.extend(Array.prototype, {
  bsearch: function(x, attr) {
    var lo = 0, hi = this.length;
    while (lo < hi) { // binary search
      var mid = Math.floor((lo + hi) / 2), val = this[mid][attr];
      if (x < val)
        lo = mid + 1;
      else
        hi = mid;
    }

    if (!this[lo] || this[lo-1] &&
      x - this[lo][attr] > this[lo-1][attr] - x)
      lo--;

    return lo;
  }
});
// another wonky binary search that returns the index of the closest value
Object.extend(Array.prototype, {
  findClosest : function(value){
    var high = this.length, low = -1, m;
    while (high - low > 1) {
      m = high + low >> 1;
      if (this[m] < value) {
        low = m;
      }
      else {
        high = m;
      }
    }
    if ((high == this.length) || ((this[high] != value) && (Math.abs(value - this[high]) >= Math.abs(value - this[high-1])))) {
      high--;
    }
    return(high);
 }
});

Object.extend(String.prototype, {
  // parseUri 1.2.2
  // (c) Steven Levithan <stevenlevithan.com>
  // MIT License
  parseUri: function() {
    var o = {
        key:    ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
        q:      { name: "queryKey", parser: /(?:^|&)([^&=]*)=?([^&]*)/g },
        parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
      },
      m   = o.parser.exec(this),
      uri = {},
      i   = 14;
    while (i--) uri[o.key[i]] = m[i] || "";
    uri[o.q.name] = {};
    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { if ($1) uri[o.q.name][$1] = $2; });
    return uri;
  }
});
// a helper function to create more dynamic object literals
// $h('key'+i, value) => { key1: value }
var $h = function() {
  var constructor = function() {
    var k = null, args = arguments[0];
    for (var i = 0; i < args.length; i++) {
      if (k == null) {
        k = args[i];
      } else {
        this[k] = args[i];
        k = null;
      }
    }
  };

  return new constructor(arguments);
};

Hash.prototype.slice = function() {
  var newHash = $H();
  for (var i = 0; i < arguments.length; i++) {
    newHash.set(arguments[i], this.get(arguments[i]));
  }
  return newHash;
};

Object.extend(Number.prototype, {
  magnitude: function() {
    return Math.floor(Math.log(this)/Math.LN10);
  },
  precision: function(max) {
    max = max || 4;
    var tmp = this, mag = this.magnitude();
    if (mag < 0) tmp *= Math.pow(10, -mag);
    var ret = tmp == Math.floor(tmp) ? 0 : ('' + tmp).split('.').last().length;
    if (ret > max) ret = max;
    if (mag < 0) ret -= mag;
    return ret;
  }
});

// allow for scrollwheel event capture
// http://andrewdupont.net/2207/11/07/pseudo-custom-events-in-prototype-16/
(function() {
  var wheel = function(event) {
    var deltaX, deltaY;

    // normalize the delta
    if('wheelDeltaY' in event) {        // WebKit
      deltaY = event.wheelDeltaY / 120;
    } else if (event.wheelDelta) {      // IE & Opera
      deltaY = event.wheelDelta / 120;
    } else if (event.detail) {          // Firefox (W3C)
      deltaY = -event.detail;
    }
    if (event.wheelDeltaX) {
      deltaX = event.wheelDeltaX / 120;
    } else if (event.axis && event.axis == event.HORIZONTAL_AXIS) {  // FF 3.5
      deltaX = -event.detail;
      deltaY = 0;
    }
    if (!deltaX && !deltaY) { return; }

    var el = Event.element(event);
    if(el && 'fire' in el) {
      var customEvent = Event.element(event).fire("mouse:wheel", {
        delta: {x: deltaX, y: deltaY},
        pointer: Event.pointer(event)
      });
      if (customEvent.stopped) { Event.stop(event); }
    }
  }
  document.observe("mousewheel",     wheel);
  document.observe("DOMMouseScroll", wheel);
})();


// Copyright (c) 2005 Thomas Fakes (http://craz8.com)
//
// This code is substantially based on code from script.aculo.us which has the
// following copyright and permission notice
//
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

var Resizeable = Class.create();
Resizeable.prototype = {
  initialize: function(element) {
    var options = Object.extend({
      top: 6,
      bottom: 6,
      left: 6,
      right: 6,
      minHeight: 0,
      minWidth: 0,
      zindex: 1000,
      onDrag: null,
      onEnd: null
    }, arguments[1] || {});

    this.element      = $(element);
    this.handle     = this.element;

    Element.makePositioned(this.element); // fix IE

    this.options      = options;

    this.active       = false;
    this.resizing     = false;
    this.currentDirection = '';

    this.eventMouseDown = this.startResize.bindAsEventListener(this);
    this.eventMouseUp   = this.endResize.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);
    this.eventCursorCheck = this.cursor.bindAsEventListener(this);
    this.eventKeypress  = this.keyPress.bindAsEventListener(this);

    this.registerEvents();
  },
  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    this.unregisterEvents();
  },
  registerEvents: function() {
    Event.observe(document, "mouseup", this.eventMouseUp);
    Event.observe(document, "mousemove", this.eventMouseMove);
    Event.observe(document, "keypress", this.eventKeypress);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
    Event.observe(this.element, "mousemove", this.eventCursorCheck);
  },
  unregisterEvents: function() {
    //if(!this.active) return;
    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
    //Event.stopObserving(document, "mousemove", this.eventCursorCheck);
    //Event.stopObserving(document, "keypress", this.eventKeypress);
  },
  startResize: function(event) {
    if(Event.isLeftClick(event)) {

      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if(src.tagName && (
        src.tagName=='INPUT' ||
        src.tagName=='SELECT' ||
        src.tagName=='BUTTON' ||
        src.tagName=='TEXTAREA')) return;

      var dir = this.directions(event);
      if (dir.length > 0) {
        this.active = true;
        var offsets = this.element.positionedOffset();
        this.startTop = offsets.top;
        this.startLeft = offsets.left;
        this.startWidth = parseInt(Element.getStyle(this.element, 'width'));
        this.startHeight = parseInt(Element.getStyle(this.element, 'height'));
        this.startX = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
        this.startY = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;

        this.currentDirection = dir;
        if (typeof(Draggables) == 'object') {
          Draggables.deactivate();
          Draggable._dragging[this.element] = true;
        }
        Event.stop(event);
      }
    }
  },
  finishResize: function(event, success) {
    // this.unregisterEvents();

    this.active = false;
    this.resizing = false;

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if (this.options.onEnd) {
      event.element = this.element;
      this.options.onEnd(event);
    }
    if (typeof(Draggables) == 'object') {
      Draggable._dragging[this.element] = false;
    }
  },
  keyPress: function(event) {
    if(this.active) {
      if(event.keyCode==Event.KEY_ESC) {
        this.finishResize(event, false);
        Event.stop(event);
      }
    }
  },
  endResize: function(event) {
    if(this.active && this.resizing) {
      this.finishResize(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.resizing = false;
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var style = this.element.style;
    if (this.currentDirection.indexOf('n') != -1) {
      var pointerMoved = this.startY - pointer[1];
      var margin = Element.getStyle(this.element, 'margin-top') || "0";
      var newHeight = this.startHeight + pointerMoved;
      if (this.options.snap) {
        newHeight = (newHeight/this.options.snap).round()*this.options.snap;
      }
      if (newHeight > this.options.minHeight) {
        style.height = newHeight + "px";
        style.top = (this.startTop - pointerMoved - parseInt(margin)) + "px";
      }
    } else if (this.currentDirection.indexOf('s') != -1) {
      var newHeight = this.startHeight + pointer[1] - this.startY;
      if (this.options.snap) {
        newHeight = (newHeight/this.options.snap).round()*this.options.snap;
      }
      if (newHeight > this.options.minHeight) {
        style.height = newHeight + "px";
      }
    }
    if (this.currentDirection.indexOf('w') != -1) {
      var pointerMoved = this.startX - pointer[0];
      var margin = Element.getStyle(this.element, 'margin-left') || "0";
      var newWidth = this.startWidth + pointerMoved;
      if (this.options.snap) {
        newWidth = (newWidth/this.options.snap).round()*this.options.snap;
      }
      if (newWidth > this.options.minWidth) {
        style.left = (this.startLeft - pointerMoved - parseInt(margin))  + "px";
        style.width = newWidth + "px";
      }
    } else if (this.currentDirection.indexOf('e') != -1) {
      var newWidth = this.startWidth + pointer[0] - this.startX;
      if (this.options.snap) {
        newWidth = (newWidth/this.options.snap).round()*this.options.snap;
      }
      if (newWidth > this.options.minWidth) {
        style.width = newWidth + "px";
      }
    }
    if (this.options.onDrag) {
      event.element = this.element;
      event.direction = this.currentDirection
      this.options.onDrag(event);
    }
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },
  between: function(val, low, high) {
    return (val >= low && val < high);
  },
  directions: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = this.element.cumulativeOffset();
    var scrollOffsets = this.element.cumulativeScrollOffset();
    offsets[0] -= scrollOffsets[0]; offsets[1] -= scrollOffsets[1];

    var cursor = '';
    if (this.between(pointer[1] - offsets[1], 0, this.options.top)) cursor = 'n';
    else if (this.between((offsets[1] + this.element.offsetHeight) - pointer[1], 0, this.options.bottom)) cursor = 's';
    if (this.between(pointer[0] - offsets[0], 0, this.options.left)) cursor += 'w';
    else if (this.between((offsets[0] + this.element.offsetWidth) - pointer[0], 0, this.options.right)) cursor += 'e';

    return cursor;
  },
  cursor: function(event) {
    var cursor = this.directions(event);
    if (cursor.length > 0) {
      cursor += '-resize';
    } else {
      cursor = '';
    }
    this.element.style.cursor = cursor;
  },
  update: function(event) {
    if(this.active) {
      if(!this.resizing) {
        var style = this.element.style;
        this.resizing = true;

        if(Element.getStyle(this.element,'position')=='')
          style.position = "relative";

        if(this.options.zindex) {
          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
          style.zIndex = this.options.zindex;
        }
      }
      this.draw(event);

      // fix AppleWebKit rendering
      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
      Event.stop(event);
      return false;
    }
  }
}

// set the time zone cookie
document.cookie = '_swivel_time_zone=' + new Date().getTimezoneOffset()*-60;


/*jslint browser: true, laxbreak: true */
var Swivel = {
  CONCEALED_STYLE: {
    display: 'block',
    position: 'absolute',
    width: '1px',
    height: '1px',
    top: '1px',
    left: '-10px'
  },
  keyDown: function() {
    return Prototype.Browser.Gecko ? 'keypress' : 'keydown';
  },

  safeColor: function(color) {
    // already in #rrggbb format?
    if (!color || color.match(/[0-9a-f]{6}/i)) return color;
    // HACK: IE really dislikes rgba
    var m = color.toLowerCase().match(/^(rgba?|hsla?)\(([\s\.\-,%0-9]+)\)/);
    if(m){
      var c = m[2].split(/\s*,\s*/), l = c.length, t = m[1];
      if((t == "rgb" && l == 3) || (t == "rgba" && l == 4)){
        var r = c[0];
        if(r.charAt(r.length - 1) == "%"){
          // 3 rgb percentage values
          var a = c.map(function(x){
            return parseFloat(x) * 2.56;
          });
          if(l == 4){ a[3] = c[3]; }
          return this._colorFromArray(a);
        }
        return this._colorFromArray(c);
      }
      if((t == "hsl" && l == 3) || (t == "hsla" && l == 4)){
        // normalize hsl values
        var H = ((parseFloat(c[0]) % 360) + 360) % 360 / 360,
          S = parseFloat(c[1]) / 100,
          L = parseFloat(c[2]) / 100,
          // calculate rgb according to the algorithm
          // recommended by the CSS3 Color Module
          m2 = L <= 0.5 ? L * (S + 1) : L + S - L * S,
          m1 = 2 * L - m2,
          a = [this._hue2rgb(m1, m2, H + 1 / 3) * 256,
            this._hue2rgb(m1, m2, H) * 256, this._hue2rgb(m1, m2, H - 1 / 3) * 256, 1];
        if(l == 4){ a[3] = c[3]; }
        return this._colorFromArray(a);
      }
    }
    return null;  // dojo.Color
  },

  _colorFromArray: function(a) {
    // summary: returns a css color string in hexadecimal representation
    if (a[3] == 0) return null; // transparent
    var arr = a.slice(0, 3).map(function(x){
      var s = parseInt(x).toString(16);
      return s.length < 2 ? "0" + s : s;
    });
    return "#" + arr.join("");  // String
  },

  // stolen from dojo for now
  _hue2rgb: function(m1, m2, h){
    if(h < 0){ ++h; }
    if(h > 1){ --h; }
    var h6 = 6 * h;
    if(h6 < 1){ return m1 + (m2 - m1) * h6; }
    if(2 * h < 1){ return m2; }
    if(3 * h < 2){ return m1 + (m2 - m1) * (2 / 3 - h) * 6; }
    return m1;
  },

  sanitizeAsDate: function(text, defaultValue) {
    var today = new Date();
    // could be 2008-04-20 or 2008 format
    text = String(text);
    text = text.replace(/-/g,'/');
    if (text.match(/^\d{4}$/)) text = text+"/01/01";
    if (text.match(/^\d{1,2}\/\d{1,2}\/\d{2}$/)) {
      text = text.split('/');
      var year = text.last();
      if (year < (today.getYear() % 100) + 50)
        text[text.length-1] = Number(year) + 2000;
      else
        text[text.length-1] = Number(year) + 1900;
      text = text.join('/');
    }
    if (text.match(/^\d{1,2}\/\d{4}$/)) { // 04/2009
      var slash = text.indexOf('/');
      text = text.slice(0, slash) + '/1' + text.slice(slash);
    }
    if (!text.match(/\d{4}/)) text = text + " " + today.getFullYear();
    var number = Date.parse(text + " GMT"); // remove time zone-ness
    if (isNaN(number)) return defaultValue;
    return number;
  },

  splitFontString: function(font) {
    if (typeof(font) == 'string') {
      var split = font.split(' ', 5);
      font = { size: parseInt(split[3]), family: split[4] };
    }
    return font;
  },

  createFontString: function(hash) {
    var size = hash.size || 10;
    var family = hash.family || "verdana";
    return "normal normal normal " + size + "px " + family;
  },

  createSelect: function(name, options, selectAction) {
    var select = new Element('select', { name: name, id: name });
    options.each(function(args) {
      var option = null;
      if (Object.isArray(args)) {
        var attributes = { value: (typeof(args[1]) == 'object') ? Object.toJSON(args[1]) : args[1] };
        if (args.size() == 3)
          Object.extend(attributes, args[2]);
        option = new Element('option', attributes).update(args[0]);
      } else {
        option = new Element('option', { disabled: 'disabled' }).update('&nbsp;');
      }
      select.appendChild(option);
    });
    if (selectAction) select.onchange = selectAction.bind(select);
    return select;
  }
};

Swivel.CopyRangeList = Class.create({
  initialize: function() {
    this._ranges = [];
  },

  push: function(range) {
    this._ranges.push(range);
  },

  reset: function() {
    this._ranges.invoke('setNoCells');
  }

});

Swivel.copyRanges = new Swivel.CopyRangeList();

Event.observe(window, 'blur', function() {
  Swivel.copyRanges.reset();
  Swivel.clipboard = null;
});

Swivel.CommentManager = Class.create({
  initialize: function(options) {
    this._div = $(options.div);
    this._personId = options.personId;
    this._commentOrder = this._div.down('.order .disabled');
    if (this._commentOrder)
      this._commentOrder = this._commentOrder.hasClassName('asc') ? 'asc' : 'desc';
    this._form = this._div.down('form');

    this._setupObservers();
  },

  _setupObservers: function() {
    // delete
    this._div.select('.comment .delete').each(function(a) {
      a.observe('click', function() {
        var id = a.up('.comment').id.replace('comment_', '');
        this.deleteComment(id);
      }.bind(this));
    }, this);

    try {
      // reorder
      this._div.select('.order a').each(function(a) {
        a.observe('click', this.rearrange.curry(a.hasClassName('asc') ? 'asc' : 'desc').bind(this));
      }, this);

      // toggle new comment form
      this._div.down('a.new_comment').observe('click', this.showNewComment.bindAsEventListener(this));
      this._form.down('a.cancel').observe('click', this.hideNewComment.bindAsEventListener(this));

      // new comment form submit
      this._form.observe('submit', this.submitNewComment.bindAsEventListener(this));
    } catch(e) {
      // won't always have these elements on every page, so be ok with errors
      // console.log(e);
    }
  },

  rearrange: function(order) {
    if (order == this._commentOrder) { return; }

    this._commentOrder = order;
    this._updatePersonPreference(this._commentOrder);
    this._positionCommentBox();

    var container = this._div.down('.comments');
    this._div.select('.comment').reverse().each(function(c) {
      container.insert(c);
    });

    this._div.select('.order a').invoke('toggleClassName', 'disabled');
  },

  _positionCommentBox: function() {
    if (!this._personId) { return; }

    if (this._commentOrder == 'desc') {
      this._div.down('.comments_header').insert({after: this._form});
    } else {
      this._div.down('.comments').insert({after: this._form});
    }
    this._form.insert({after: this._div.down('a.new_comment')});
  },

  _updatePersonPreference: function() {
    var url =  '/people/' + this._personId + '.json';
    var params = { "person[comment_order]": this._commentOrder };
    new Ajax.Request(url, {
      method: 'put',
      parameters: params
    });
  },

  showNewComment: function(e) {
    new Effect.toggle(this._form, 'blind', {
      afterFinish: function() {
        if (this._commentOrder == 'asc') {
          new Effect.ScrollTo(this._form);
        }
        this._form.down('textarea').activate();
      }.bind(this)
    });

    this._div.down('a.new_comment').hide();
  },

  hideNewComment: function() {
    this._form.blindUp().reset();
    this._div.down('a.new_comment').show();
  },

  submitNewComment: function(e) {
    var progress = this._form.down('.progress');
    new Ajax.Request(this._form.getAttribute('action') + '.js', {
      parameters: this._form.serialize(true),
      onLoading: function() { progress.appear(); },
      onComplete: function() { progress.fade(); },
      onSuccess: this.insertNewComment.bind(this),
      onFailure: function(t) {
        new Swivel.NoticeDialog(t.responseText);
      }
    });
  },

  insertNewComment: function(t) {
    this.hideNewComment();

    var comments = this._div.select('.comment'), insert = 'top';
    if (this._commentOrder == 'asc') {
      insert = 'bottom';
    }
    this._div.down('.comments').insert($h(insert, t.responseText));
    this.reStripeAll();

    var newComments = this._div.select('.comment');
    newComments.without.apply(newComments, comments).each(function(c) {
      c.down('.delete').observe('click', function() {
        var id = c.id.replace('comment_', '');
        this.deleteComment(id);
      }.bind(this));
    }, this);
  },

  deleteComment: function(id) {
    new Swivel.OKCancelDialog('Are you sure?', 'Are you sure you want to delete this comment?', function(ok) {
      if (ok) {
        new Ajax.Request('/comments/' + id, { method: 'delete' });
        this._div.down('#comment_' + id).remove();
        this.reStripeAll();
      }
    }.bind(this));
  },

  reStripeAll: function() {
    var comments = this._div.select('.comment');
    comments.each(function(c, i) {
      c.removeClassName('odd'); c.removeClassName('even');
      c.addClassName(i & 1 ? 'odd' : 'even');
    });
    var count = this._div.down('.count');
    if (count)
      count.update(comments.size());
  }
});


Swivel.connect = function(src, srcMethod, dest, destMethod, options) {
  if (!src || !src[srcMethod]) return;

  options = options || {};
  var fn = src[srcMethod], observers = { before: [], after: [] };
  if (!fn._observers) {
    var replacement = function() {
      var before = observers.before, after = observers.after,
          i, d, method;
      // before
      for (i = 0; i < before.length; i++) {
        d = before[i];
        method = d.method;
        if (typeof(method) != 'function')
          method = d.obj[d.method];
        method.apply(d.obj, arguments);
      }

      // call the original function
      var r = fn.apply(src, arguments);

      // after
      for (i = 0; i < after.length; i++) {
        d = after[i];
        method = d.method;
        if (typeof(method) != 'function')
          method = d.obj[d.method];
        method.apply(d.obj, arguments);
      }

      return r;
    };
    replacement._observers = observers;

    src[srcMethod] = replacement;
  } else {
    observers = fn._observers;
  }

  var list = options.before ? observers.before : observers.after;
  return list.push({ obj: dest, method: destMethod });
};
Swivel.disconnect = function(src, srcMethod, handle) {
  var fn = src && src[srcMethod];
  if (fn && fn._observers) {
    [fn._observers.before, fn._observers.after].each(function(observers) {
      var i = observers.indexOf(handle);
      if (i != -1)
        observers.splice(i, 1);
    });
  }
};

Swivel.status = function(msg, options) {
  var s = $('status');

  if (options && options.loading)
    msg = '<img src="/images/icons/progress_sm.gif" class="progress" /> ' + msg;
  s.update(msg);

  if (!s.visible())
    s.appear();
};

Swivel.Undoable = Class.create({
  initialize: function(obj, initFn) {
    this._obj = obj;
    this._obj.undo = this._undo.bind(this);
    this._obj.redo = this._redo.bind(this);

    this._stack = new Swivel.Undoable.Stack();
    this._callbacks = [];

    if (initFn) { initFn(this); }
  },
  observe: function(method, fn) {
    Swivel.connect(this._obj, method, this, function() {
      var obj = this._obj, args = arguments;
      this._defaultRedo = function() { obj[method].apply(obj, args); };
      fn.apply(obj, args);
    }, {before: true});
  },
  undo: function(undo, options) {
    if (!this._isUndoing) {
      var action = Object.extend({
        undo: undo,
        redo: this._defaultRedo
      }, options);
      this._stack.push(action);
    }
  },
  callback: function(callback) {
    this._callbacks.push(callback);
  },

  // private
  _undo: function() {
    this._isUndoing = true;
    try {
      if (this._stack.canDown()) {
        this._stack.down().undo.apply(this._obj);
        this._callbacks.each(function(c) { c(); });
      }
    } finally {
      this._isUndoing = false;
    }
  },
  _redo: function() {
    this._isUndoing = true;
    try {
      if (this._stack.canUp()) {
        this._stack.up().redo.apply(this._obj);
        this._callbacks.each(function(c) { c(); });
      }
    } finally {
      this._isUndoing = false;
    }
  }
});

Swivel.Undoable.Stack = Class.create({
  initialize: function() {
    this._stack = [];
    this._index = -1;
  },

  push: function(obj) {
    this._stack = this._stack.slice(0, this._index+1);  // clear pending redos
    this._stack.push(obj);
    this._index++;
  },

  canDown: function() {
    return this._index >= 0;
  },
  down: function() {
    return this._stack[this._index--];
  },
  canUp: function() {
    return this._stack[this._index + 1];
  },
  up: function() {
    return this._stack[++this._index];
  }
});

Swivel.UndoManager = Class.create({
  initialize: function() {
    this._stack = new Swivel.Undoable.Stack();
  },
  addUndoable: function(undoable) {
    Swivel.connect(undoable, 'undo', this, function() {
      if (this._isUndoing) return;
      this._stack.push(undoable);
    });
  },

  undo: function() {
    if (!this._stack.canDown()) return;

    this._isUndoing = true;
    try { this._stack.down()._undo(); }
    finally { this._isUndoing = false; }
  },

  redo: function() {
    if (!this._stack.canUp()) return;

    this._isUndoing = true;
    try { this._stack.up()._redo(); }
    finally { this._isUndoing = false; }
  }
});


/*
new Swivel.Tabs('tabs', {
  cleanWhitespace: true,
  tabs: [
    { name: 'Sheet 1', id: 'sheet_1' },
    { name: 'Sheet 2', id: 'sheet_2' }
  ]
});
 */
Swivel.Tabs = Class.create({
  initialize: function(div, options) {
    this.callbacks = { };

    this.container = $(div);
    this.panels = { };

    if (options && options.tabs) {
      // generate everything dynamically (assumes tabs below)
      this.panels = { };
      this.list = new Element('ul', { 'class': 'tabs bottom' });
      var content = new Element('div', { 'class': 'content' });

      this.container.insert(content);
      this.container
        .insert(new Element('div', { 'class': 'bottom' })
          .insert(new Element('div', { 'class': 'bottom', id: 'row_range_display' }).update('&nbsp;')) // TODO: can't use id here
          .insert(this.list));

      options.tabs.each(function(t, i) {
        var tab = new Element('li')
          .insert(new Element('a', { href: '#' + t.id }).update(t.name));
        if (i == 0) tab.addClassName('first selected');
        this.list.insert(tab).insert(' ');

        var panel = new Element('div', { 'class': t.id });
        content.insert(panel);
        this.panels['#' + t.id] = panel;
      }, this);
    } else {
      // discover them in the dom
      this.container.select('.content > div').each(function(p) {
        this.panels['#' + p.id] = p;
        p.addClassName(p.id);
        p.id = '';  // TODO: this is pretty iff-y, but prevents the browser from jumping.  use class names instead?
      }, this);

      this.list = this.container.down('ul.tabs');
    }

    this.tabs = this.list.select('a');
    this.tabs.each(function(a) {
      a.observe('click', this.activate.bind(this, a.hash));
    }, this);

    if (options && options.cleanWhitespace)
      this.list.cleanWhitespace();
    if (this.list.getWidth() < this.list.scrollWidth)
      this.setupScrolling();
    var hash = window.location.hash;
    if (!this.panels[hash]) hash = this.tabs[0].hash;
    this.activate.curry(hash).bind(this).defer();
  },

  getPanel: function(id) {
    return this.panels['#' + id];
  },

  activate: function(hash) {
    // highlight the right tab
    this.tabs.each(function(a) {
      if (a.hash == hash)
        $(a.parentNode).addClassName('selected');
      else
        $(a.parentNode).removeClassName('selected');
    });

    // show the right panel
    $H(this.panels).values().invoke('hide');
    this.panels[hash].show();
    if ($('saving_progress'))
      $('saving_progress').setStyle({display : 'none'});

    // fire off any events
    // TODO: thread?
    if (this.callbacks[hash])
      this.callbacks[hash](this.panels[hash]);
  },

  observe: function(id, callback) {
    this.callbacks['#' + id] = callback;
  },

  remove: function(id) {
    var tab = this.tabs.find(function(t) { return t.hash == '#' + id; });
    this.panels['#' + id].fade({afterFinish: (function() {
      $(tab.parentNode).remove();

      this.tabs = this.tabs.without(tab);
      this.activate(window.location.hash = this.tabs[0].hash);
    }).bind(this)});
  },

  setupScrolling: function() {
    var arrows = new Element('div', { 'class': 'arrows' }).
      insert(new Element('a', { href: '#', onclick: 'return false;', 'class': 'button grouped first' }).
        update('<img src="/images/icons/left_arrow.png"/>').
        observe('click', this.scroll.curry(-1).bind(this))).
      insert(new Element('a', { href: '#', onclick: 'return false;', 'class': 'button grouped last' }).
        update('<img src="/images/icons/right_arrow.png"/>').
        observe('click', this.scroll.curry(1).bind(this)));

    this.list.insert({ before: arrows });
  },

  scroll: function(direction) {
    direction = direction || 1;
    var t = new Effect.Tween(this.list,
      this.list.scrollLeft,
      this.list.scrollLeft + direction * this.list.getWidth(),
      'scrollLeft');
  }
});

Swivel.Dialog = Class.create({
  initialize: function(div, options) {
    this.options = {};
    this.options.chrome = (options && options.chrome !== undefined) ? options.chrome : true;
    this.options.modal = (options && options.modal !== undefined) ? options.modal : false;
    this.options.topPosition = (options && options.topPosition !== undefined) ? options.topPosition : false;

    if(this.options.chrome) {
      // setup a dialog container
      var dialog = new Element("div", {
        'class': 'dialog'
      });

      this.div = new Element("div", {
        'class': 'dialog_container',
        'style': 'display: none'
      }).update(dialog);
    } else {
      this.div = $(div);
    }
    this.div.setStyle({zIndex : 101});

    $(document.body).insert(this.div);

    // must come after body insertion (annoying)
    if(this.options.chrome) {
      dialog.update($(div));
      $(div).show();
    }

    if (this.do_show)
      this.show();
  },

  show: function(options) {
    if (!this.div) {
      this.do_show = true;
      return;
    }

    if(this.options.modal) {
      this.blocker = new Element('div', { 'class' : 'blocker' }).hide();
      $(document.body).insert(this.blocker);
      this.blocker.appear({ to: 0.2 });

      $w('keypress keydown mousedown mousemove click dblclick').each(function(e) {
        this.blocker.observe(e, Event.stop);
      }, this);
      this.div.observe('mousemove', Event.stop);

      var scrollHeight = $(document.body).scrollHeight;
      if (scrollHeight > this.blocker.getHeight())
        this.blocker.setStyle({ height: scrollHeight + 'px' });
    }

    var offset = document.viewport.getScrollOffsets();
    var panel_dim = this.div.getDimensions();
    var viewport_dim = $(document.viewport).getDimensions();

    var parentOffset =  Element.cumulativeOffset(this.div.getOffsetParent());
    var topPos = offset[1] - parentOffset[1] +
      (viewport_dim.height - panel_dim.height) / 4;

    var leftPos = offset[0] - parentOffset[0] +
      (viewport_dim.width - panel_dim.width) / 2;

    if (this.options.topPosition !== false) {
      topPos = this.options.topPosition;
    }

    this.div.setStyle({
      top: topPos + "px",
      left: leftPos + "px"
    });

    this._show(options);
  },

  _show: function(options) {
    this.div.appear(options);
  },

  hide: function() {
    this._hide();
    if (this.blocker) {
      this.blocker.fade({ afterFinish: this.blocker.remove.bind(this.blocker) });
      this.div.stopObserving('mousemove', Event.stop);

      this.blocker = null;
    }
    this.div.select('form').each(Form.reset);
  },

  _hide:function(){
    this.div.hide();
  },

  toggle: function() {
    if (this.div.visible()) {
      this.hide();
    } else {
      this.show();
    }
  }
});

Swivel.NoticeDialog = Class.create(Swivel.Dialog, {
  initialize: function($super, msg, delay, callback) {
    var div = new Element('div', { style: 'display: none;' }).
      update('<p>' + msg + '</p>');

    this.teardown = function() {
      this.hide();
      if (callback) callback();
    }.bind(this);

    var closeButton = new Element('input', { type: 'button', value: 'Close' }).
      observe('click', this.teardown.curry());
    div.insert(closeButton);

    $super(div);

    this.show({
      afterFinish: function() { closeButton.focus(); }
    });
    this.hide.bind(this).delay(delay || 5.5);
  }
});

Swivel.SheetDialog = Class.create(Swivel.Dialog, {
  initialize: function($super, div, callback, options) {
    var container = new Element('div', { 'class': 'sheet-dialog' }).hide();
    $(document.body).insert(container);

    div = $(div);
    container.update(div);

    options = Object.extend({ chrome: false, modal: true, topPosition: 0 }, options);
    $super(container, options);

    $(div).select('.close').invoke('observe', 'click', this.hide.bind(this));

    div.show();
    this.show();
  },

  _hide: function() {
    if (this.div.down()) {
      this.div.slideUp();
    }
  },

  _show: function() {
    var firstInput = this.div.down('input[type=text], textarea, input[type=submit], input[type=button]')
    this.div.slideDown({
      afterFinish: function() {
        if (firstInput)
          firstInput.activate();
      }
    });
  }
});

Swivel.ModalDialog = Class.create(Swivel.Dialog, {
  initialize: function($super, div, callback, options, buttons) {
    div = $(div);
    buttons = buttons || div.select("button, input[type='button']");

    this.teardown = function(b) {
      this.hide();
      Event.stopObserving(document, Swivel.keyDown());
      if (callback) callback(b);
    }.bind(this);

    $super(div, Object.extend({chrome: false, modal: true}, options || {}));

    buttons.each(function(b) {
      b.observe('click', this.teardown.curry(b));
    }, this);
    this.show({
      afterFinish: function() { buttons.first().focus(); }
    });
    Event.observe(document, Swivel.keyDown(), function(e){
      if (e.keyCode == 27) { this.teardown(); } // escape
    }.bind(this));
  }
});

Swivel.ControlDialog = Class.create(Swivel.Dialog, {
  initialize: function($super, div, callback, options, buttons) {
    div = $(div);
    buttons = buttons || div.select("button, input[type='button']");

    var teardown = function(b) {
      this.hide();
      if (callback) callback(b);
    }.bind(this);

    $super(div, Object.extend({chrome: false, modal: true}, options || {}));

    buttons.each(function(b) {
      b.observe('click', teardown.curry(b));
    });

    this.show({
      afterFinish: function() { if(buttons.length > 0) buttons.first().focus(); }
    });
  }
});

Swivel.OKCancelDialog = Class.create(Swivel.SheetDialog, {
  initialize: function($super, title, msg, callback) {
    var div = new Element('div');

    div.insert(new Element('h1').update(title));

    var ok = new Element('input', { type: 'button', value: 'OK', 'class': 'close'});
    var cancel = new Element('a', { href: '#', 'class': 'close' }).update('Cancel');
    cancel.onclick = function() { return false; };

    div.insert(new Element('div', {'class': 'major form'}).
      insert(new Element('p').insert(msg)).
      insert(ok).
      insert(' ').
      insert(cancel));

    if (callback) {
      ok.observe('click', callback.curry(true));
      cancel.observe('click', callback.curry(false));
    }

    $super(div);
  }
});


Swivel.Listing = Class.create({
  initialize: function(options) {
    this._table = $(options.table);
    this._filters = $(options.filters);
    this._selectFilter = options.selectFilter;
    this._personId = options.personId;
    this._headers = options.header;
    this._controls = options.controls;

    this._setupRows();
    this._setupObservers();
    this._setupControls();
  },

  showAll: function() {
    if (this._headers)
      this._headers.invoke('show')
    this._rows.invoke('show');
    this._reStripe();
  },

  showOnly: function(cls) {
    if (cls == 'all') {
      this.showAll();
      return;
    }
    var lastVisible = null;
    if (this._headers) {
      this._headers.invoke('show')
    }
    this._rows.each(function(tr) {
      if (!tr.hasClassName(cls)) {
        tr.hide();
      } else if (!tr.visible()) {
        tr.show();
      }
    });
    this. _hideEmptyHeaders();
    this._reStripe();
  },

  _setupRows: function() {
    if (!this._table) {
      this._rows = [];
      return;
    }

    this._style = this._table.classNames().find(function(c) { return c !== 'list'; }) || 'tiles';

    if (this._selectFilter) {
      this._rows = this._table.select(this._selectFilter);
    } else {
      this._rows = this._table.select('tr').reject(function(tr) {
        return tr.down('th');
      });
    }
  },

  remove: function(cls) {
    var separated = this._rows.partition(function(tr) {
      return tr.hasClassName(cls);
    });

    separated[0].invoke('blindUp');
    this._rows = separated[1];
    this._reStripe();
  },

  _setupObservers: function() {
    this.updateFilters();
    setInterval(this.updateFilters.bind(this), 200);
  },

  updateFilters: function() {
    var tab = window.location.href.split(/#/)[1];
    if (tab) {
      $$('.filters li').each(function(li) {
        li.removeClassName('current');
        if (li.className.match(tab)) {
          li.addClassName('current');
        }
      });
      var cls = 'all';
      if (tab == 'charts') {
        cls = 'Chart';
      } else if (tab == 'workbooks') {
        cls = 'Workbook';
      } // TODO: translating between these names is needlessly cumbersome
      this.showOnly(cls);
    }
  },

  _setupControls: function() {
    if (!this._controls) { return; }

    var tb = new Swivel.Toolbar({
      div: this._controls,
      items: [
        { id: 'listing_style_tiles',
          title: 'Tile view', toggle: true, group: 'list_style',
          'class': 'listing_style_tiles first grouped',
          callback: function() { this._setStyle('tiles'); }.bind(this)
        },
        { id: 'listing_style_thumbnail',
          title: 'Thumbnail view', toggle: true, group: 'list_style',
          'class': 'listing_style_thumbnail grouped',
          callback: function() { this._setStyle('thumbnail'); }.bind(this)
        },
        { id: 'listing_style_compact',
          title: 'List view', toggle: true, group: 'list_style',
          'class': 'listing_style_compact last grouped',
          callback: function() { this._setStyle('compact'); }.bind(this)
        }
      ]
    });
    tb.getItem('listing_style_' + this._style).select(true);
  },

  _setStyle: function(pref) {
    if (pref == this._style) { return; }
    var tiles = this._table.previous('#tiles');

    this._table.removeClassName(this._style);
    tiles.removeClassName(this._style);

    this._style = pref;

    this._table.addClassName(this._style);
    tiles.addClassName(this._style);

    this._updatePersonPreference();
  },

  _updatePersonPreference: function() {
    if (!this._personId) return;
    var url =  '/people/' + this._personId + '.json';
    var params = { "person[listing_style]": this._style };
    new Ajax.Request(url, {
      method: 'put',
      parameters: params
    });
  },

  _hideEmptyHeaders: function() {
    var allRows = this._table.select('tr').reject(function(t) { return t.hasClassName('space')});
    var shouldHide = true;
    var size = allRows.size();
    for (var i = size -1; i > 0 ; i--) {
      if (!allRows[i].hasClassName('day') && allRows[i].visible()) {
          shouldHide = false;
      }
      if(allRows[i-1].hasClassName('day')) {
        i--;
        if (shouldHide && allRows[i].visible()) {
          allRows[i].hide();
        }
        shouldHide = true;
      }
    }

  },

  _reStripe: function() {
    var visible = this._rows.select(function(tr) { return tr.visible(); });
    visible.each(function(tr, i) {
      if ((i & 1) == 0) { // even
        tr.removeClassName('odd');
        tr.addClassName('even');
      } else { // odd
        tr.removeClassName('even');
        tr.addClassName('odd');
      }
    });
  }
});

Swivel.Toolbar = Class.create({
  initialize: function(options) {
    this._name = options.name;
    this._items = [];
    this._itemsById = {};

    this._createElements();
    this._setupObservers();

    if (this._name)
      this.addItems([this._name, ' ', ' ']);

    if (options.items)
      this.addItems(options.items);

    if (options.div)
      this.render(options.div);
  },

  addItems: function(items) {
    items.each(function(e) {
      if (typeof(e) == 'string') {
        if (e == '-') {
          this.addSpacer();
          this.addSpacer();
          this.addSeparator();
          this.addSpacer();
          this.addSpacer();
        } else if (e == ' ') {
          this.addSpacer();
        } else {
          this.addItem(e);
        }
      } else if (e.render) {
        this.addItem(e);
      } else { // assume button config hash
        this.addItem(new Swivel.Toolbar.Button(e));
      }
    }, this);
  },

  addItem: function(item) {
    this._items.push(item);
    if(item._id != null) this._itemsById[item._id] = item;

    var td = new Element('td');
    td.addClassName(item._id);

    if (item.setContainer)
      item.setContainer(this._row);

    if (item.render) {
      item.render(td);
    } else {
      td.update(item);
    }

    this._row.insert(td);
  },

  getItem: function(id) {
    return this._itemsById[id];
  },

  getItems: function() {
    return this._items;
  },

  getItemGroup: function(group) {
    return this._items.select(function(item) { return item._group == group; });
  },

  addSpacer: function() {
    this._row.insert('<td class="spacer"></td>');
  },

  addSeparator: function() {
    this._row.insert('<td class="separator"></td>');
  },

  render: function(div) {
    this._div = $(div);
    this._div.update(this._table);
  },

  _createElements: function() {
    this._row = new Element('tr');
    this._table = new Element('table', {'class': 'toolbar'}).
      insert(new Element('tbody').
        insert(this._row));
  },

  _setupObservers: function() {
    this._row.observe('button:select', function(e) {
      var button = e.memo.source;
      if(button._group) {
        var groupButtons = this.getItemGroup(button._group).reject(function(item) { return item == button; });
        groupButtons.invoke('select', false);
      }
    }.bindAsEventListener(this));
  }
});

Swivel.Toolbar.Item = Class.create({
  initialize: function(options) {
    this._id = options.id;
  },

  setContainer: function(c) {
    this._container = c;
  },

  getContainer: function(c) {
    return this._container;
  }
});

Swivel.Toolbar.Helper = Class.create({
  moreButtons: {},
  defaultOptions: function(options) {
    var target = options.target;
    return $H({
      'data_text':
        { id: 'data_text', 'class': 'data_text grouped first', group: 'data', allowDeselect: true,
        menu: {
          items: this.DATA_TEXT_ITEMS,
          callback: target.setNumericFormatSelection.bind(target)
        }
      },
      'data_decimal':
        { id: 'data_decimal', 'class': 'data_decimal grouped', group: 'data', allowDeselect: true,
        menu: {
          items: this.DATA_DECIMAL_ITEMS,
          callback: target.setNumericFormatSelection.bind(target)
        }
      },
      'data_currency':
        { id: 'data_currency', 'class': 'data_currency grouped', group: 'data', allowDeselect: true,
        menu: {
          items: this.DATA_CURRENCY_ITEMS,
          callback: target.setNumericFormatSelection.bind(target)
        }
      },
      'data_percentage':
        { id: 'data_percentage', 'class': 'data_percentage grouped', group: 'data', allowDeselect: true,
        menu: {
          items: this.DATA_PERCENTAGE_ITEMS,
          callback: target.setNumericFormatSelection.bind(target)
        }
      },
      'data_date':
        { id: 'data_date', 'class': 'data_date grouped last', group: 'data', allowDeselect: true,
        menu: {
          contents: this.dataDateFormElements(),
          callback: target.setDataDateFormat.bind(target)
        }
      }
    });
  },

  advancedOptions: function(options) {
    var target = options.target;
    return $H({
      'data_precision':
        { id: 'data_precision', 'class': 'data_precision grouped first',
          menu: {
            contents: this.dataPrecisionFormElements(),
            callback: target.setDataAdvancedFormat.bind(target)
          }
        },
      'data_negative':
        { id: 'data_negative', 'class': 'data_negative grouped',
          menu: {
            contents: this.dataNegativeFormElements(),
            callback: target.setDataAdvancedFormat.bind(target)
          }
        },
      'data_magnitude':
        { id: 'data_magnitude', 'class': 'data_magnitude grouped',
          menu: {
            contents: this.dataMagnitudeFormElements(),
            callback: target.setDataAdvancedFormat.bind(target)
          }
        },
      'data_separator':
        { id: 'data_separator', 'class': 'data_separator grouped last',
          menu: {
            contents: this.dataSeparatorElements(),
            callback: target.setDataSeparatorFormat.bind(target)
          }
        }
    });
  },

  updateFormatButtons: function(toolbar, format) {
    if (!format) format = {};
    var simpleButtons = ['data_text', 'data_decimal', 'data_currency', 'data_percentage'];
    simpleButtons.each(function(t) {
      var b = toolbar.getItem(t);
      if (b) b.setOptions(format);
    });

    var dateButton      = toolbar.getItem('data_date');
    var precisionButton = toolbar.getItem('data_precision' )|| this.moreButtons['data_precision'];
    var magnitudeButton = toolbar.getItem('data_magnitude' )|| this.moreButtons['data_magnitude'];
    var negativeButton  = toolbar.getItem('data_negative'  )|| this.moreButtons['data_negative'];
    var separatorButton = toolbar.getItem('data_separator' )|| this.moreButtons['data_separator'];
    var dateString = "", timeString = "";
    if (dateButton) {
      if (format.dt) {
        var tokens = format.dt.split(/(%H|%I|%-1I)/);
        dateString = tokens[0].strip();
        if (tokens.size() > 1)
          timeString = (tokens[1] + tokens[2]).strip();
        dateButton.setOptions({
          data_date_date: dateString,
          data_date_time: timeString});
      }
      dateButton.select(format.dt);
    }

    format = $H(format);
    if (precisionButton) precisionButton.setOptions({data_precision: format.slice('p').toJSON()});
    if (magnitudeButton) magnitudeButton.setOptions({data_magnitude: format.slice('mag').toJSON()});
    if (negativeButton ) negativeButton.setOptions( {data_negative: format.slice('ns', 'np', 'nc').toJSON()});
    if (separatorButton) separatorButton.setOptions({
      data_separator_prefix: format.slice('pfx').toJSON(),
      data_separator_separator: format.slice('c').toJSON(),
      data_separator_suffix: format.slice('sfx').toJSON()
    });
  },

  dataDateFormElements: function() {
    return new Element('table').
      insert(new Element('tbody').
        insert(new Element('tr').
          insert('<td><label for="data_date_date">Date</label></td>').
          insert(new Element('td').
            insert(Swivel.createSelect('data_date_date', this.DATA_DATE_ITEMS)))).
        insert(new Element('tr').
          insert('<td><label for="data_date_time">Time</label></td>').
          insert(new Element('td').
            insert(Swivel.createSelect('data_date_time', this.DATA_TIME_ITEMS)))));
  },

  dataPrecisionFormElements: function(selectAction) {
    var precisionOptions =
      [ ["",  {"p":-1}],
        ["0", {"p": 0}],
        ["1", {"p": 1}],
        ["2", {"p": 2}],
        ["3", {"p": 3}],
        ["4", {"p": 4}],
        ["5", {"p": 5}],
        ["6", {"p": 6}],
        ["7", {"p": 7}] ];
    var titleLabel = selectAction ? '' : '<label>Number of Decimals</label><br/>'
    var select = Swivel.createSelect('data_precision', precisionOptions, selectAction);
    var div = new Element('div').
      insert(titleLabel).
      insert(select);
    div.setOptions = this._setOptions.bind(div);
    return div;
  },

  dataMagnitudeFormElements: function(selectAction) {
    var magnitudeOptions =
      [ ["Ones (default)",  {"mag": 0} ],
        ["Tens",            {"mag": 1} ],
        ["Hundreds",        {"mag": 2} ],
        ["Thousands",       {"mag": 3} ],
        ["Millions",        {"mag": 6} ],
        ["Billions",        {"mag": 9} ],
        ["Trillions",       {"mag": 12} ] ];
    var titleLabel = selectAction ? '' : '<label>Order of Magnitude</label><br/>'
    var select = Swivel.createSelect('data_magnitude', magnitudeOptions, selectAction);
    var div = new Element('div').
      insert(titleLabel).
      insert(select);
    div.setOptions = this._setOptions.bind(div);
    return div;
  },

  dataNegativeFormElements: function(selectAction) {
    var negativeOptions =
      [ ["-1234",   {"ns": true, "np": false, "nc": false}, {"style": 'color:#000;'}],
        ["(1234)",  {"ns": false, "np": true, "nc": false}, {"style": 'color:#000;'}],
        ["-1234 (red)",   {"ns": true, "np": false, "nc": "Red"}, {"style": 'color:Red;'}],
        ["(1234) (red)",  {"ns": false, "np": true, "nc": "Red"}, {"style": 'color:Red;'}] ];
    var titleLabel = selectAction ? '' : '<label>Negative Number Format</label><br/>'
    var select = Swivel.createSelect('data_negative', negativeOptions, selectAction);
    var div = new Element('div').
      insert(titleLabel).
      insert(select);
    div.setOptions = this._setOptions.bind(div);
    return div;
  },

  dataSeparatorElements: function(selectAction) {
    var prefixOptions =
      [ ["None",    {"pfx": ""}],
        ["$",       {"pfx": "$"}],
        ["&euro;",  {"pfx": "&euro;"}] ],
        separatorOptions =
        [ ["None",    {"c": false}],
          [",",       {"c": true}] ],
        suffixOptions =
        [ ["None",    {"sfx": ""}],
          ["%",       {"sfx": "%"}],
          ["&euro;",  {"sfx": "&euro;"}],
          ["K",       {"sfx": "K"}],
          ["M",       {"sfx": "M"}],
          ["B",       {"sfx": "B"}] ];
    var titleLabel = selectAction ? '' : '<label>Prefix, Separator & Suffix</label><br/>';
    var div = new Element('div').
      insert(titleLabel).
      insert(new Element('table').
        insert(new Element('tbody').
          insert(new Element('tr').
            insert('<td><label class="label">Prefix</label class="label"></td><td><label class="label">Separator</label class="label"></td><td><label class="label">Suffix</label class="label"></td>')).
          insert(new Element('tr').
            insert(new Element('td').
              update(Swivel.createSelect('data_separator_prefix', prefixOptions, selectAction))).
            insert(new Element('td').
              update(Swivel.createSelect('data_separator_separator', separatorOptions, selectAction))).
            insert(new Element('td').
              update(Swivel.createSelect('data_separator_suffix', suffixOptions, selectAction))))));
    div.setOptions = this._setOptions.bind(div);
    return div;
  },

  _setOptions: function(options) {
    $H(options).each(function(pair) {
      var input = this.down("*[name="+ pair.key +"]");
      input.selectedIndex = 0;
      input.value = pair.value;
    }, this);
    return false;
  },

  DATA_TEXT_ITEMS:
    [ ["Plain Text", {t: "t"} ] ],
  DATA_DECIMAL_ITEMS:
    [ ["1234",      {t: "n", p: 0, c: false, ns: true} ],
      ["1234.19",   {t: "n", p: 2, c: false, ns: true} ],
      "-",
      ["1,234",     {t: "n", p: 0, c: true,  ns: true} ],
      ["1,234.19",  {t: "n", p: 2, c: true,  ns: true} ],
      "-",
      ["1.234E+03", {t: "e", p: 3, ns: true} ] ],
  DATA_CURRENCY_ITEMS:
    [ ["$1234",     {t: "c", p: 0, c: false, np: true,  ns: false, nc: 'Red' } ],
      ["$1234.19",  {t: "c", p: 2, c: false, np: true,  ns: false, nc: 'Red' } ],
      "-",
      ["$1,234",    {t: "c", p: 0, c: true,  np: true,  ns: false, nc: 'Red' } ],
      ["$1,234.19", {t: "c", p: 2, c: true,  np: true,  ns: false, nc: 'Red' } ] ],
  DATA_PERCENTAGE_ITEMS:
    [ ["12%",       {t: "p", p: 0, c: true } ],
      ["12.34%",    {t: "p", p: 2, c: true } ] ],
  DATA_DATE_ITEMS:
    [ ["None",                  ""],
      "-",
      ["Monday",                "%A"],
      ["Mon",                   "%a"],
      "-",
      ["March",                 "%B"],
      ["Mar",                   "%b"],
      "-",
      ["2008",                  "%Y"],
      ["08",                    "%y"],
      "-",
      ["Monday, March 5, 2008", "%A, %B %-1d, %Y"],
      ["Mon, Mar 5, 2008",      "%a, %b %-1d, %Y"],
      ["Mon, Mar 05, 2008",     "%a, %b %d, %Y"],
      ["March 5, 2008",         "%B %-1d, %Y"],
      "-",
      ["3/5/2008",              "%-1m/%-1d/%Y"],
      ["3/5/08",                "%-1m/%-1d/%y"],
      ["3/5",                   "%-1m/%-1d"],
      "-",
      ["03/05/2008",            "%m/%d/%Y"],
      ["03/05/08",              "%m/%d/%y"],
      ["03/2008",               "%m/%Y"],
      ["03/05",                 "%m/%d"],
      "-",
      ["5-Mar-2008",            "%-1d-%b-%Y"],
      ["5-Mar-08",              "%-1d-%b-%y"],
      ["5-Mar",                 "%-1d-%b"],
      "-",
      ["March-2008",            "%B-%Y"],
      ["March-08",              "%B-%y"],
      ["Mar-08",                "%b-%y"],
      "-",
      ["Q1",                    "%q"],
      ["Q1 08",                 "%q %y"],
      ["Quarter 1",             "%Q"],
      ["Quarter 1 2008",        "%Q %Y"] ],
  DATA_TIME_ITEMS:
      [ ["None",                  ""],
        "-",
        ["1:30 PM",               "%-1I:%M %p"],
        ["1:30PM",                "%-1I:%M%p"],
        ["1:30 pm",               "%-1I:%M %p%p"],
        ["1:30pm",                "%-1I:%M%p%p"],
        "-",
        ["1:30:59 PM",            "%-1I:%M:%S %p"],
        ["1:30:59PM",             "%-1I:%M:%S%p"],
        ["1:30:59 pm",            "%-1I:%M:%S %p%p"],
        ["1:30:59pm",             "%-1I:%M:%S%p%p"],
        "-",
        ["01:30 PM",              "%I:%M %p"],
        ["01:30PM",               "%I:%M%p"],
        ["01:30 pm",              "%I:%M %p%p"],
        ["01:30pm",               "%I:%M%p%p"],
        "-",
        ["01:30:59 PM",           "%I:%M:%S %p"],
        ["01:30:59PM",            "%I:%M:%S%p"],
        ["01:30:59 pm",           "%I:%M:%S %p%p"],
        ["01:30:59pm",            "%I:%M:%S%p%p"],
        "-",
        ["13:30",                 "%H:%M"],
        ["13:30:59",              "%H:%M:%S"] ]
});

Swivel.Toolbar.Checkbox = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    $super(options);
    var checkbox = new Element('input', {
      id: options.id,
      type: 'checkbox'
    });
    checkbox.defaultChecked = !!options.value;
    var label = new Element('label', {'for': options.id}).update(options.label).setStyle({whiteSpace:"nowrap"});
    this._div = new Element('div').insert(checkbox).insert(label).setStyle({whiteSpace:"nowrap"});

    if (options.callback) {
      checkbox.observe('click', options.callback);
    }
  },

  render: function(element) {
    $(element).insert(this._div);
  }
});

Swivel.Toolbar.TimeRangePicker = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    $super(options);
    this._fromDate    = new Element('input', {type: 'text', size: 12});
    this._toDate      = new Element('input', {type: 'text', size: 12});
    this._leftButton  = new Element('button', {'class': 'left'}).update("&nbsp;");
    this._rightButton = new Element('button', {'class': 'right'}).update("&nbsp;");
    this._extrema = options.extrema;
    this.setRange(options.timerange);
    this._div = new Element('div').
      insert(this._leftButton).
      insert(this._fromDate).
      insert(" to ").
      insert(this._toDate).
      insert(this._rightButton).
      setStyle({whiteSpace: 'nowrap'});
    if (options.callback) {
      this._callback = options.callback;
      this._fromDate.onchange = function() { this._callback(this.getRange()); }.bind(this);
      this._toDate.onchange   = function() { this._callback(this.getRange()); }.bind(this);
    }
    this._leftButton.onclick = this.moveLeft.bind(this);
    this._rightButton.onclick = this.moveRight.bind(this);
  },

  moveLeft: function()  {
    this.move(true);
  },

  moveRight: function() {
    this.move(false);
  },

  move: function(movingLeft) {
    var timerange = this.getRange();
    var diff = timerange.to - timerange.from;
    if (diff < 1) return;

    if (movingLeft) {
      timerange.to = timerange.from
      timerange.from = timerange.from - diff;
    } else {
      timerange.from = timerange.to;
      timerange.to = timerange.to + diff;
    }

    this.setRange(timerange);
    this._callback(this.getRange());
  },

  getRange: function() {
    var fromValue = Swivel.sanitizeAsDate(this._fromDate.value);
    var toValue   = Swivel.sanitizeAsDate(this._toDate.value);
    return {from: fromValue, to: toValue};
  },

  setRange: function(timerange) {
    // translate to our format here
    this._fromDate.value  = new Date(timerange.from).strftime("%m/%d/%Y");
    this._toDate.value    = new Date(timerange.to  ).strftime("%m/%d/%Y");
    try {
      this._fromDate.blur(); // otherwise kinda ugly when it's focused already
      this._toDate.blur();
    } catch(e) {
      // TODO: unhack for IE
    }
    if (timerange.from <= this._extrema.min) {
      this._leftButton.disabled = true;
    } else {
      this._leftButton.disabled = false;
    }
    if (timerange.to >= this._extrema.max) {
      this._rightButton.disabled = true;
    } else {
      this._rightButton.disabled = false;
    }
  },

  render: function(element) {
    $(element).insert(this._div);
    if (this._menu)
      this._menu.render(element);
  }
});

Swivel.Toolbar.Button = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    $super(options);

    this._label = options.label;
    this._class = options['class'];
    this._title = options.title;
    this._toggle = options.toggle;
    this._value = options.value;
    this._group = options.group;
    this._allowDeselect = options.allowDeselect || !options.group;
    this._callback = options.callback;
    this._elementId = options.elementId;

    this._createElements();
    this._setupObservers();

    if (options.menu) {
      // automatically try to make menu objects out of the menu options
      if (options.menu.items)
        this._menu = new Swivel.Toolbar.SelectMenu(this, options.menu);
      else if (options.menu.contents)
        this._menu = new Swivel.Toolbar.DialogMenu(this, options.menu);
      else if (options.menu.listItems)
        this._menu = new Swivel.Toolbar.ListMenu(this, options.menu);
      else if (options.menu.show)  // already a menu
        this._menu = options.menu;
      else
        this._menu = new Swivel.Toolbar.Menu(this, options.menu);
    }
    if (options.hidden) {
      this.hide();
    }
    if (options.disabled)
      this.disable();
  },

  _createElements: function() {
    this._button = new Element('button', { 'class': this._class });
    if (this._elementId) this._button.id = this._elementId;
    if (this._title) { this._button.title = this._title; }
    this._content = new Element('div').update(this._label);
    this._button.update(this._content);
  },

  _setupObservers: function() {
    this._button.observe('mousedown', function(e) { e.stop(); }); // prevents focus
    this._button.observe('click', this._onClick.bindAsEventListener(this));
  },

  disable: function() {
    return this.setEnabled(false);
  },

  enable: function() {
    return this.setEnabled(true);
  },

  setEnabled: function(enable) {
    this._button.disabled = !enable;
    return this;
  },

  render: function(element) {
    $(element).insert(this._button);
    if (this._menu)
      this._menu.render(element);
  },

  select: function(on) {
    this._value = on;
    if (on) {
      this._button.addClassName('selected');
      this._container.fire('button:select', { source: this });
    } else {
      this._button.removeClassName('selected');
      if (this._menu) {
        this._menu.deselect();
      }
    }
  },

  toggle: function() {
    if (this._toggle) {
      this._value = !this._value;
      this.select(this._value);
    }

    if (this._menu) {
      if (this._menu.visible())
        this.hide();
      else
        this.show();
    }
  },

  show: function() {
    if (this._menu) {
      this._menu.show();
    }
  },

  hide: function() {
    if (this._menu)
      this._menu.hide();
  },

  setOptions: function(format) {
    this.select(this._menu.setOptions(format));
  },

  setLabel: function(label) {
    this._label = label;
    this._content.update(this._label);
  },

  _onClick: function(e) {
    if (!this._allowDeselect && this._value)
      return;

    this.toggle();

    if (this._callback) {
      this._callback(e);
    }
  }
});

Swivel.Toolbar.Select = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    $super(options);

    this._value = options.value;
    this._items = options.items;

    this._select = new Element('select');
    $A(options.items).each(function(o) {
      var option = new Element('option', { value: o.value }).update(o.name);
      if (o.value == this._value)
        option.selected = 'selected';
      if (o.style)
        option.setStyle(o.style);
      this._select.insert(option);
    }, this);

    if (options.callback)
      this.observe('change', options.callback);

    if (options.disabled)
      this.disable();
  },

  setSelected: function(value){
    this._select.value = value;
  },

  observe: function(type, callback) {
    this._select.observe(type, callback);
  },

  render: function(element) {
    $(element).insert(this._select);
  },

  disable: function() {
    this._select.disable();
  },

  enable: function() {
    this._select.enable();
  }
});


Swivel.Toolbar.Input = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    $super(options);

    var styles = {
      border: 'none',
      padding: 0,
      margin: 0,
      outlineWidth: 0,
      fontSize: '10px'
    };
    if (Prototype.Browser.IE) {styles.paddingBottom = '1px';}
    if (options.align && ['left', 'right', 'center'].include(options.align)) {
      styles.textAlign = options.align;
    }

    this._input = new Element('input', {type:'text', size: 2, value: options.value});
    this._input.setStyle(styles);
    if (Prototype.Browser.WebKit) { // put blue glow outline around whole div
      this._input.onfocus = function() { this._div.setStyle({outline: 'auto 3px rgb(75,137,208)'}); }.bind(this);
      this._input.onblur  = function() { this._div.setStyle({outlineWidth: '0px'}); }.bind(this);
    }
    var onclick = function() { this._input.focus(); };

    // fake text input box
    this._div = new Element('div', {'class': ''}).setStyle({
      backgroundColor: '#ffffff',
      border: '1px solid #CCCCCC',
      margin: 0,
      padding: '2px',
      fontSize: '10px'
    });

    this._div.onclick = onclick.bind(this);
    if (options.labels && options.labels.before)
      this._div.insert(options.labels.before);

    this._div.insert(this._input)

    if (options.labels && options.labels.after) {
      var label = new Element('label').update(options.labels.after);
      label.onclick = onclick.bind(this);
      this._div.insert(label);
    }

    if (options.callback)
      this.observe('change', options.callback);

    if (options.disabled)
      this.disable();

  },

  observe: function(type, callback) {
    this._input.observe(type, callback);
  },

  render: function(element) {
    $(element).insert(this._div);
  },

  disable: function() {
    this._input.disable();
    this._div.setStyle({backgroundColor: '#eee'});
  },

  enable: function() {
    this._input.enable();
    this._div.setStyle({backgroundColor: '#fff'});
  }
});

Swivel.Toolbar.FontFamilySelect = Class.create(Swivel.Toolbar.Select, {
  initialize: function($super, options) {
    $super(Object.extend(options || {}, {
      value: options.value || 'helvetica',
      items: this.FONTS
    }));
  },

  FONTS:
    ['Arial', 'Courier', 'Futura', 'Helvetica', 'Times', 'Verdana'].map(function(font) {
      return { value: font.toLowerCase(), name: font};
    })
});

Swivel.Toolbar.FontSizeSelect = Class.create(Swivel.Toolbar.Select, {
  initialize: function($super, options) {
    $super(Object.extend(options || {}, {
      items: options.sizes || this.SIZES
    }));
  },

  SIZES:
    $w('6 7 8 9 10 11 12 13 14 15 16 18 20 24 32 48').map(function(s) {
      return { value: s, name: s };
    })
});

// TODO inherit as much from Button as possible, as there is duplication here
Swivel.Toolbar.ColorPicker = Class.create(Swivel.Toolbar.Item, {
  initialize: function($super, options) {
    options = options || {};
    $super(options);
    this._value = options.value;

    var content = new Element('div');
    if (this._value) {
      if (this._value.color)
        this._value = this._value.color;
      this._value = Swivel.safeColor(this._value);
      content.setStyle({ 'backgroundColor': this._value });
    }
    this._title = options.title;
    this._button = new Element('button', { 'class': 'swatch', title: this._title }).update(content);
    this._button.observe('click', function(e) {
      if (this._getPalette().visible())
        this.hide();
      else
        this.show();
    }.bindAsEventListener(this));
    this._div = new Element('div', {'class': 'color_picker'});
    this._div.insert(this._button);
    this._div.observe('mousedown', function(e) { e.stop(); });

    this._observers = [];
    this._boundOnDocumentClick = this._onDocumentClick.bindAsEventListener(this);

    if (options.callback) { this.observe(options.callback); }
    if (options.disabled) { this.disable(); }

    Swivel.Toolbar.ColorPicker.RECENT_COLORS.register(this);
  },

  render: function(element) {
    $(element).insert(this._div);
    return this;
  },

  setColor: function(color) {
    color = Swivel.safeColor(color);
    this._button.down().setStyle({ backgroundColor: color });
  },

  show: function() {
    this._getPalette().appear();
    this._div.setStyle({zIndex: 2});
    document.observe('click', this._boundOnDocumentClick);
  },

  hide: function() {
    //don't change this to fade(), it will break IE
    this._getPalette().hide();
    this._div.setStyle({zIndex: 1});
    document.stopObserving('click', this._boundOnDocumentClick);
  },

  observe: function(f) {
    this._observers.push(f);
    return this;
  },

  disable: function() {
    return this.setEnabled(false);
  },

  enable: function() {
    return this.setEnabled(true);
  },

  setEnabled: function(enable) {
    this._button.disabled = !enable;
    return this;
  },

  addRecentColor: function(c) {
    if (this._recentRow && this._recentRow.down('td')) {
      this._recentRow.down('td:last-child').remove();
      this._recentRow.insert({ top: this._createSwatch(c) });
    }
  },

  _onClick: function(e) {
    var td = e.findElement('td');
    if (td && td.hasClassName('swatch')) {
      var c = td.getStyle('backgroundColor');
      this._button.down().setStyle({ backgroundColor: c });
      this._setCurrentSwatch(td);

      this._observers.each(function(f) { f(c); });
      Swivel.Toolbar.ColorPicker.RECENT_COLORS.push(c);

      this.hide();
    }
  },

  _onDocumentClick: function(e) {
    if (e.findElement('button') != this._button) {
      this.hide();
    }
  },

  _setCurrentSwatch: function(s) {
    if (this._currentSwatch) { this._currentSwatch.removeClassName('current'); }
    this._currentSwatch = s;
    if (this._currentSwatch) { this._currentSwatch.addClassName('current'); }
  },

  _getPalette: function() {
    if (!this._palette) {
      var tbody = new Element('tbody');
      this._COLORS.inGroupsOf(this._COLORS_PER_ROW, -1).each(function(row) {
        var tr = '<tr>';
        row.each(function(c) {
          if (c != -1)
            tr += this._createSwatch(c);
        }, this);
        tbody.insert(tr);
      }, this);

      var table = new Element('table').update(tbody);

      this._recentRow = new Element('tr');
      Swivel.Toolbar.ColorPicker.RECENT_COLORS.getColors().each(function(c) {
        this._recentRow.insert(this._createSwatch(c));
      }, this);
      var recent = new Element('div', { 'class': 'recent' }).
        insert(new Element('table', {'class': 'recent'}).
          update(new Element('tbody').update(this._recentRow))).
        insert('Recent');

      this._palette = new Element('div', { 'class': 'palette' });
      this._palette.setStyle({ display: 'none' });
      this._palette.insert(table).insert(recent);
      this._palette.observe('click', this._onClick.bindAsEventListener(this));

      if (this._value) {
        this._setCurrentSwatch(this._palette.down('td.' + this._value.substring(1)));
      }
      this._div.insert(this._palette);
    }

    return this._palette;
  },

  _createSwatch: function(c) {
    if (c) {
      var cls = c.substring(1);
      var rgb = [
        parseInt(c.substring(1,3), 16),
        parseInt(c.substring(3,5), 16),
        parseInt(c.substring(5,7), 16)
      ].join(', ');
      return '<td class="swatch ' + cls + '" style="background-color:' + c + ';" title=" RGB (' + rgb + ') "></td>';
    } else {
      return '<td class="swatch nocolor" style="background-color:transparent;" title="no color"></td>';
    }
  },

  _COLORS_PER_ROW: 16,
  _COLORS: [ "#f30e10", "#fbff15", "#33ff0c", "#41fefe", "#2400fd", "#f700fe", "#ffffff", "#e6e6e6", "#dadada", "#cecece", "#c1c1c1", "#b5b5b5", "#a9a9a9", "#9c9c9c", "#8f8f8f", "#838383",
"#de1a20", "#fff500", "#00973f", "#0099ea", "#281c7f", "#de007a", "#767676", "#696969", "#5c5c5c", "#4f4f4f", "#434343", "#363636", "#292929", "#1c1c1c", "#0e0e0e", "#000000",
"#eebeb0", "#f3ceb8", "#f5ddc2", "#fffdd7", "#dfe6d6", "#d5d9d5", "#baccbf", "#b4cccb", "#b1dcf3", "#b0bbcf", "#acaebd", "#b1aebf", "#bab4bf", "#bfbabf", "#ebccdb", "#ebcfd0",
"#ec8768", "#f19f70", "#f6bc78", "#fff989", "#b8da8a", "#95cc8b", "#76c08b", "#71c1bb", "#6ac2f3", "#7194cf", "#747ebe", "#776baf", "#8f70b0", "#ac77b2", "#eb87b5", "#ec878c",
"#e55b40", "#eb7e46", "#f2a24d", "#fff758", "#9dcc60", "#6ebb63", "#3bab65", "#31ada4", "#1aadee", "#4075be", "#4a5ca9", "#504597", "#724897", "#944d98", "#e4589a", "#e55a6b",
"#de1a20", "#e4561f", "#ec861d", "#fff500", "#7dbe32", "#38a93a", "#00973f", "#00998b", "#0099ea", "#005aad", "#133d95", "#281c7f", "#53197e", "#7c147c", "#de007a", "#de104a",
"#850d10", "#883410", "#8d510f", "#989208", "#49751e", "#1e6925", "#006129", "#006057", "#006091", "#00376c", "#09235e", "#180851", "#34004f", "#4e004d", "#87004c", "#86002c",
"#610000", "#632400", "#663a00", "#6d6a00", "#335514", "#104d19", "#00471c", "#004740", "#00476b", "#002650", "#001545", "#0f003b", "#25003a", "#380038", "#630036", "#61001e",
"#b8a488", "#857462", "#5e5046", "#413732", "#292422", "#b58c5c", "#916b41", "#76512c", "#5f3d1c", "#4b2c11", null ]
});
Swivel.Toolbar.ColorPicker._RecentColors = Class.create({
  initialize: function(max) {
    this._colors = [];
    for (var i = 0; i < max; i++) {
      this._colors[i] = '#ffffff';
    }
    this._pickers = [];
  },

  getColors: function() {
    return this._colors;
  },

  push: function(color) {
    if (!this._colors.include(color)) {
      this._colors.unshift(color);
      this._pickers.invoke('addRecentColor', color);

      this._colors.pop();
    }
  },

  register: function(picker) {
    this._pickers.push(picker);
  }
});
Swivel.Toolbar.ColorPicker.RECENT_COLORS = new Swivel.Toolbar.ColorPicker._RecentColors(11);

Swivel.Toolbar.Menu = Class.create({
  initialize: function(button, options) {
    this._button = button;
    this._callback = options.callback;
    this._createElements();
    this._setupObservers();
    this._boundOnDocumentClick = this._onDocumentClick.bindAsEventListener(this);
  },

  _createElements: function() {
    this._div = new Element('div', { 'class': 'menu' });
    this._div.setStyle({ display: 'none' });
  },

  _setupObservers: function() {
  },

  render: function(element) {
    $(element).insert(this._div);
  },

  visible: function() {
    return this._div.visible();
  },

  deselect: function() {
  },

  show: function() {
    this._div.appear();
    (function(){
      var left = [(document.viewport.getDimensions().width - this._div.getWidth() - 10), this._div.offsetLeft].min();
      this._div.style.left = left  +'px';
    }).bind(this).delay(.1);
    document.observe('click', this._boundOnDocumentClick);
  },

  hide: function() {
    this._div.fade();
    document.stopObserving('click', this._boundOnDocumentClick);
  },

  setOptions: function(format) {
  },

  _onDocumentClick: function(e) {
    var container = $(this._button._button.parentNode);
    var element = e.element();
    var ancestors = element.ancestors ? element.ancestors() : [];
    if (!ancestors.include(container)) {
      this.hide();
    }
  }
});

Swivel.Toolbar.SelectMenu = Class.create(Swivel.Toolbar.Menu, {
  initialize: function($super, button, options) {
    $super(button, options);
    if (options.items)
      this.addItems(options.items);
  },

  _createElements: function($super) {
    $super();
    this._div.addClassName('select');

    this._ul = new Element('ul');
    this._div.insert(this._ul);
  },

  deselect: function() {
    this._ul.select('li').invoke('removeClassName', 'selected');
  },

  _setupObservers: function($super) {
    $super();
    this._ul.observe('click', this._onClick.bindAsEventListener(this));
  },

  addItems: function(items) {
    items.each(function(item) {
      var li;
      if (item === '-') {
        this._ul.insert('<hr/>');
      } else if (Object.isArray(item)) {
        li = new Element('li').update(item[0]);
        li._value = item[1];
        this._ul.insert(li);
      } else {
        li = new Element('li').update(item);
        li._value = item;
        this._ul.insert(li);
      }
    }, this);
  },

  setOptions: function(format) {
    this._ul.descendants().each(function(li, i) {
      if (!li._value || typeof(li._value) == 'string') return;
      for (k in li._value) {
        if (li._value[k] != format[k]) {
          li.removeClassName('selected');
          return;
        }
      }
      li.addClassName('selected');
    },this);
    return this._ul.firstDescendant()._value.t == format.t;
  },

  _onClick: function(e) {
    var li = e.element();
    if (!li._value)
      return;
    this._ul.descendants().each(function(l) {
      l.removeClassName('selected');
    });
    li.addClassName('selected');
    this._button.select(true);
    if (this._callback)
      this._callback(li._value);
    this.hide();
  }
});

Swivel.Toolbar.ListMenu = Class.create(Swivel.Toolbar.Menu, {
  initialize: function($super, button, options) {
    $super(button, options);

    if (options.listItems)
      this.addItems(options.listItems);
  },

  _createElements: function($super) {
    $super();
    this._div.addClassName('checkbox');

    this._ul = new Element('ul', {'class': 'dialog'});
    this._div.insert(this._ul);
  },

  addItems: function(items) {
    items.each(function(item, i) {
      if (item === '-') {
        this._ul.insert('<hr/>');
      } else {
        this._ul.insert(new Element('li').update(item));
      }
    }, this);
  }
});

Swivel.Toolbar.DialogMenu = Class.create(Swivel.Toolbar.Menu, {
  initialize: function($super, button, options) {
    this._contents = options.contents;
    $super(button, options);
  },

  _createElements: function($super) {
    $super();
    this._div.addClassName('dialog');
    this._form = new Element('form');
    this._form.onsubmit = function() { return false;};
    this._form.insert(this._contents);

    var buttons = new Element('div', { 'class': 'buttons' });

    var ok = new Element('input', { type: 'submit', value: 'OK' });
    buttons.insert(ok).insert(' ');

    var cancel = new Element('a', { href: '#', onclick: 'return false;' }).update('Cancel');
    cancel.observe('click', this.hide.bindAsEventListener(this));
    buttons.insert(cancel);

    this._form.insert(buttons);

    this._div.insert(this._form);
  },

  _setupObservers: function($super) {
    $super();
    this._form.observe('submit', this._onSubmit.bindAsEventListener(this));
  },

  setOptions: function(options) {
    $H(options).each(function(pair) {
      var input = this._form.down("*[name="+ pair.key +"]");
      input.selectedIndex = 0;
      input.value = pair.value;
    }, this);
    return false;
  },

  _onSubmit: function(e) {
    this._button.select(true);
    if (this._callback)
      this._callback(this._form.serialize(true));
    this.hide();
  }
});


Swivel.Toolbar.SelectionDialog = Class.create({
  initialize: function(f, callback, children, buttons) {

    // location gathered in a hack-ish way
    var isDate = (f == 'date');
    var isFirst = (f == 'precision');
    var isLast = (f == 'separator') || isDate;
    var border = isFirst ? "": "with_3_borders";
    this.children = children;

    this.span = new Element('span', {'class' : 'selection_dialog_span ' + border, 'id' : 'sheet_number_format_'+f });
    this.span.set = isDate ? this.selectDateTime.bind(this) : this.select.bind(this);
    this.span.unset = this.selectNone.bind(this);
    this.span.update(new Element('img', {'src' : '/images/tool_bar/data_'+f+'.png'}));
    if (isLast) this.span.appendChild(new Element('img', {'src': "/images/tool_bar/arrow.png", 'class': 'down_arrow'}));

    var html = new Element('div', {'class': 'dialog', 'id': 'selection_dialog_selects_' + f});
    this.children.each(function(c) {this.createSelectWithLabel(html, c, true);}.bind(this));

    var buttons = new Element('div', {'id': 'selection_dialog_buttons_' + f, 'style' : 'text-align: right;'}).update('<hr style="width:90%;"/>');
    var button_ok = new Element('input', {'type': "submit", 'value': "Ok"}).observe('click', function(){this.submit();}.bind(this));
    var button_no = new Element('input', {'type': "reset", 'value': "Cancel"}).observe('click', function(){this.span.deactivate();}.bind(this));
    buttons.appendChild(button_no);
    buttons.appendChild(button_ok);

    this.div = new Element('div', {'class' : 'selection_dialog', 'id': 'selection_dialog_div_' + f, 'style' : 'display:none;width:250px;'});
    this.div.appendChild(html);
    this.div.appendChild(buttons);
    this.div.observe('mousedown', function(e) { if (e.target.nodeName.toLowerCase() != "select") Event.stop(e); });

    this.observers = [];
    if (callback) this.observe(callback);
    $(document.body).insert(this.div);

    this.span.observe('click', this.show.bind(this));
    this.span.activate = this.show.bind(this);
    this.span.deactivate = this.hide.bind(this);
  },

  createSelectWithLabel: function(html, name, float_left) {
    var select = new Swivel.Toolbar.AdvancedSelect(name);
    var label_class = 'label';
    if (float_left) label_class += ' float';
    html.appendChild(new Element('div', {'class': label_class}).update(this.formatLabelChoices(name)));
    html.appendChild(select.html());
    if (float_left) html.appendChild(new Element('div', {'style': 'clear:left;'}));
  },

  formatLabelChoices: function(f) {
    var formatLabelChoices = {
      "date": "Date",
      "time": "Time",
      "precision": "Number of Decimals",
      "negative": "Negative Number Format",
      // "magnitude": "Order of Magnitude",
      "prefix": "Prefix",
      "separator": "Separator",
      "suffix": "Suffix"
    };
    return formatLabelChoices[f];
  },

  hide: function() {
    this.div.hide();
  },

  html: function() {
    return this.span;
  },

  observe: function(f) {
    this.observers.push(f);
    return this;
  },

  select: function(format) {
    this.children.each(function(c) {
      var select = $('selection_dialog_select_' + c);
      select.set(format);
    });
  },

  // for date/time we would need to compare startsWith and endsWith
  selectDateTime: function(format) {
    // no need to look if t!=d or missing dt
    if (format["t"] != "d" || !format["dt"] || format["dt"] == '') return;

    var dateSelect = $('selection_dialog_select_date');
    var timeSelect = $('selection_dialog_select_time');
    dateSelect.selectedIndex = 0;
    timeSelect.selectedIndex = 0;

    for (i=0; i<dateSelect.options.length; i++) {
      if (dateSelect.options[i].disabled) continue;
      var dt = dateSelect.options[i].value.evalJSON()["dt"];
      if (dt == "") continue;
      if (format["dt"].startsWith(dt)) {
        dateSelect.selectedIndex = i;
        break;
      }
    }

    for (i=0; i<timeSelect.options.length; i++) {
      if (timeSelect.options[i].disabled) continue;
      var dt = timeSelect.options[i].value;
      if (dt == "") continue;
      if (format["dt"].endsWith(dt)) {
        timeSelect.selectedIndex = i;
        break;
      }
    }
  },

  selectNone: function() {
    this.children.each(function(c) {
      $('selection_dialog_select_' + c).selectedIndex = 0; // default
    });
  },

  show: function() {
    if (this.span.disabled) return;
    var left, top;
    if(Prototype.Browser.IE) {
      var rect = this.span.getBoundingClientRect();
      left = rect.left + 10;
      top = rect.bottom + 1;
    } else {
      left = this.span.offsetLeft + 9;
      top = this.span.offsetTop + this.span.offsetHeight + 1;
    }
    this.div.style.left = left + 'px';
    this.div.style.top = top + 'px';
    this.div.show();
    // too far right
    var viewport_width = document.viewport.getDimensions().width;
    if (left + this.div.clientWidth > viewport_width) {
      this.div.style.left = (viewport_width - this.div.clientWidth) + 'px';
    }
  },

  submit: function() {
    this.observers.each(function(f) { f(); });
    this.span.deactivate();
  }
});

Swivel.InPlaceEditor = Class.create({
  initialize: function(element, handler) {
    this._element = $(element);
    this._handler = handler;

    this._element.addClassName('inplaceeditable');
    this._element.observe('click', this._onClick.bindAsEventListener(this));
  },

  _onClick: function(e) {
    var boundSubmit = this._onSubmit.bindAsEventListener(this),
        boundCancel = this._onCancel.bindAsEventListener(this);

    var input = new Element('input', { type: 'text', value: this._element.innerHTML });
    input.observe('keydown', boundCancel);
    input.observe('blur', function() { boundSubmit.defer(); });
    this._form = new Element('form', { 'class': 'inplace' });
    this._form.onsubmit = function() {return false; };
    this._form.observe('submit', boundSubmit);
    this._form.insert(input);

    this._element.replace(this._form);
    input.activate();
  },

  _onSubmit: function() {
    if (this._form) {
      var value = $F(this._form.down('input'));

      this._form.replace(this._element.update(value));
      this._form = null;

      this._handler(value);
    }
  },

  _onCancel: function(e) {
    if (e.keyCode == Event.KEY_ESC && this._form) {
      this._form.replace(this._element);
      this._form = null;
    }
  }
});
// new Swivel.RestInPlaceEditor($('chart_title'), '/charts/1', 'charts[title]');
Swivel.RestInPlaceEditor = Class.create(Swivel.InPlaceEditor, {
  initialize: function($super, element, url, field) {
    this._url = url;
    this._field = field;

    $super(element, this._onUpdate.bind(this));
  },

  _onUpdate: function(value) {
    var params = {};
    params[this._field] = value;

    new Ajax.Request(this._url, {
      method: 'put',
      parameters: params
    });
  }
});

Swivel.Cookie = {
  write: function(name, value, expireDays) {
    var exdate = new Date();
    exdate.setTime(exdate.getTime() + expireDays * 24 * 60 * 60 * 1000);
    document.cookie = escape(name) + "=" + escape(value) +
      (expireDays == null ? "" : ";expires=" + exdate.toGMTString());
  },

  read: function (name) {
    if (document.cookie.length <= 0) return null;

    name = escape(name);

    var start = document.cookie.indexOf(name + "=");
    if (start == -1) return null;
    start = start + name.length + 1;

    var end = document.cookie.indexOf(";", start);
    if (end == -1) end = document.cookie.length;

    return unescape(document.cookie.substring(start, end));
  },

  clear: function(name) {
    Swivel.Cookie.write(name, '', -1);
  }
};

Swivel.Keyboard = Class.create({
  initialize: function(options) {
    opts = {
      _debug: false,
      // if set to false, you can have multiple callbacks for a single event (if you had ctrl+k and meta+k for instance)
      _onlyOneCallback: true,
      _keydown: Swivel.keyDown(),
      _allShortcuts: []
    };
    Object.extend(opts, options || {});
    Object.extend(this, opts);
  },

  observe: function(shortcuts) {
    shortcuts.each( this._addShortcut.bind(this) );
  },

  /**
   * Nomenclature
   *  "ctrl+a"          : ctrl, meta, alt, and shift are allowed
   *  "esc"             : for special chars, look at #keys
   *  "ctrl+[a-zA-Z]"   : square brackets indicate regex. use double backslashes
   *
   * Options
   *  "condition"       : only run if condition function is met
   *  "metaOrCtrl"      : by default, meta and ctrl keys are treated the same
   *  "ignoreShift"     : ctrl+a and ctrl+shift+a should not be treated the same
   *  "propagate"       : allows the event to be propagated if set to true
   */
  _addShortcut: function(shortcut) {
    if (shortcut.name) {
      var s = {
        condition: function() {return true;},
        ignoreShift: false,
        metaOrCtrl: true,
        print: false,
        propagate: false
      };
      Object.extend(s, this._parseShortcut(shortcut));
      this._allShortcuts.push(s);
    }
  },

  _log: function(x) {
    if (this._debug) {
      if (Prototype.Browser.IE) {
        // alert(x);
      } else {
        console.log(x);
      }
    }
  },

  dispatch: function(ev) {
    if (![16, 17, 18, 20].include(ev.keyCode)) this._log('-> dispatched(k,c) = '+ev.type+'(' + ev.keyCode + ", " + ev.charCode + ')');
    var eventItem = this._parseEvent(ev);
    if (eventItem) {
      this._allShortcuts.any(function(shortcut) {
        if (this._equals(shortcut, eventItem)) {
          this._log('\tshortcut = ' + shortcut.name + ", " + shortcut.condition() + ", " + !!shortcut.callback);
          if (shortcut.condition() && shortcut.callback) {
            this._log('here, ' + shortcut.print + ":"+this._convertToCharacter(eventItem, true));
            if (shortcut.print) {
              if (!Prototype.Browser.Gecko && (this._lastKeyCode == eventItem.keyCode)) {
                this._lastKeyCode = null;
                return false;
              }
              this._lastKeyCode = null; // clear here too
              // IE/WK should ignore keydown for printable chars
              if (Prototype.Browser.Gecko || eventItem.type == 'keypress') {
                shortcut.callback(this._convertToCharacter(eventItem, true));
              }
              else return false;
            } else {
              // IE should ignore keypress for nonprintable chars
              if (Prototype.Browser.Gecko || eventItem.type == 'keydown') {
                this._log("keydown");
                shortcut.callback();
                if (!Prototype.Browser.Gecko) {
                  this._lastKeyCode = eventItem.keyCode; // suppress next keydown for printing chars
                }
              }
              else return false;
            }
          }
          if (!shortcut.propagate) {
            ev.stop();
          }
          return this._onlyOneCallback;
        }
        return false;
      }, this);
    }
  },

  _equals: function(shortcut, eventItem) {
    var meta = shortcut.modifiers.meta  == eventItem.modifiers.meta;
    var ctrl = shortcut.modifiers.ctrl  == eventItem.modifiers.ctrl;
    if (shortcut.metaOrCtrl) {
      meta = (shortcut.modifiers.meta || shortcut.modifiers.ctrl) ==
            (eventItem.modifiers.meta || eventItem.modifiers.ctrl);
      ctrl = meta;
    }
    var alt  = shortcut.modifiers.alt   == eventItem.modifiers.alt;
    var shift = shortcut.ignoreShift || (shortcut.modifiers.shift == eventItem.modifiers.shift);

    if (! (meta && ctrl && alt && shift)) return false;

    if (shortcut.key.match(/^\[.+\]$/)) {
      if (eventItem.key.match(eval("/" + shortcut.key + "/"))) {
        return eventItem.printable;
      } else {
        return false;
      }
    }

    return shortcut.key == eventItem.key.toLowerCase();
  },

  geckoKeepKeyDown: function(e) {
    var type = ((e.keyCode == 32 && e.ctrlKey) || (e.altKey)) ? 'keydown' : 'keypress';
    return e.type == type;
  },

  _parseEvent: function(ev) {
    if (Prototype.Browser.Gecko) {
      if (!this.geckoKeepKeyDown(ev)) {
        return false;
      }
    }
    var swEvent = {};
    swEvent.type = ev.type;
    swEvent.keyCode = ev.keyCode;
    swEvent.charCode = ev.charCode;
    if (Prototype.Browser.Gecko && swEvent.type == 'keydown') {
      swEvent.charCode = swEvent.keyCode; // ctrl + space issue
    }
    swEvent.modifiers = {
      ctrl:   !!ev.ctrlKey,
      alt:    !!ev.altKey,
      meta:   !!ev.metaKey,
      shift:  !!ev.shiftKey
    };
    swEvent.printable = this._convertToCharacter(swEvent);
    swEvent.key = swEvent.printable || this._specialKeys(swEvent.keyCode);
    if (swEvent.key == "ignore") return false;
    if (this._debug) {
      var fullName = (swEvent.modifiers.meta ? "meta+"  : "") +
        (swEvent.modifiers.ctrl ? "ctrl+" : "") +
        (swEvent.modifiers.alt  ? "alt+"  : "") +
        (swEvent.modifiers.shift? "shift+": "") +
        swEvent.key;
      this._log("\tevent = [ " + fullName + " ]" + (swEvent.printable ? " => " + swEvent.printable : ""));
    }
    if (swEvent.key === undefined || swEvent.key === null) swEvent.key = '';
    return swEvent;
  },

  _convertToCharacter: function(ev, unShifted) {
    if (!this._printable(ev)) return null;
    var c = String.fromCharCode(Prototype.Browser.Gecko ? ev.charCode : ev.keyCode);

    if (Prototype.Browser.Gecko && ev.charCode == 31) return '-'; // special case in ctrl+-
    if (ev.shiftKey && !unShifted) {
      return this._downShift(c); // shift + chars
    }
    return c;
  },

  _downShift: function(c) {
    var from = $A("~!@#$%^&*()_+{}|:\"<>?");
    var to   = $A("`1234567890-=[]\\;',./");
    var index = from.indexOf(c);
    if (index == -1) return c;
    return to[index];
  },

  _toLowerCase: function(c) {
    if (!c.match(/[A-Z]/)) return c;
    return c.toLowerCase();
  },

  _specialKeys: function(keyCode) {
    var ignore = {16: 'shift', 17: 'meta', 18: 'alt'};
    if (ignore[keyCode]) return "ignore";

    var keys = {
        8: 'delete', 12: 'delete', 46: 'delete',
        9: 'tab',
       13: 'enter',  27: 'escape',
       33: 'pgup',   34: 'pgdn',
       35: 'end',    36: 'home',
       37: 'left',   38: 'up',     39: 'right',  40: 'down',
      112: 'f1',    113: 'f2',    114: 'f3',    115: 'f4',    116: 'f5',
      117: 'f6',    118: 'f7',    119: 'f8',    120: 'f9',    121: 'f10',
      122: 'f11',   123: 'f12',   124: 'f13',   125: 'f14',   126: 'f15',
      127: 'f16',   128: 'f17',   129: 'f18',   130: 'f19',
      186: ';',     187: '=',     188: ',',     189: '-',     190: '.',     191: '/',     192: '`',
      219: '[',     220: "\\",    221: ']',     222: "'"
    };
    for (var i=0; i<10; i++) keys[48+i] = String(i);
    return keys[keyCode];
  },

  _printable: function(ev) {
    var code = Prototype.Browser.Gecko ? ev.charCode : ev.keyCode;
    // ignore these printable chars for keydown in IE and WK
    if (!Prototype.Browser.Gecko && ev.type == 'keydown') {
      if ( 33 <= code && code <= 40) return false; // arrows
      if ( 46 <= code && code <= 57) return false; // shift+0-9
      if (112 <= code && code <=126) return false; // f1-f19
    }
    if ( 31 <= code && code <= 126) return true; // 31 is '-' in FF when ctrl is pressed
    return false; // none of these
  },

  _parseShortcut: function(shortcut) {
    var name = shortcut.name;
    var modifiers = {
      meta:   !! name.match(/meta/),
      ctrl:   !! name.match(/ctrl/),
      'alt':  !! name.match(/alt/),
      shift:  !! name.match(/shift/)
    };
    var values = {
      key: name.split('+').last(),
      modifiers: modifiers
    };
    Object.extend(values, shortcut);
    return values;
  }
});

// Swivel.queue(function() { do something; Swivel.dequeue(); });
(function() {
  var queue = [];
  Swivel.dequeue = function() {
    fn = queue.shift();
    if (fn)
      fn();
  };
  Swivel.queue = function(fn) {
    queue.push(fn);
    if (queue.length == 1) {
      queue[0] = Swivel.dequeue;
      fn();
    }
  };
  Swivel.queuedAjax = function(url, options) {
    options = options || {};
    var old = options.onSuccess;
    options.onSuccess = function() {
      if (old) old.apply(this, arguments);
      Swivel.dequeue();
    };
    Swivel.queue(function(){ new Ajax.Request(url, options); })};
})();

/**
*
*  AJAX IFRAME METHOD (AIM)
*  http://www.webtoolkit.info/
*
**/

AIM = {

  frame : function(c) {
    var n = 'f' + Math.floor(Math.random() * 99999);
    var d = document.createElement('DIV');
    d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+n+'" name="'+n+'" onload="AIM.loaded(\''+n+'\')"></iframe>';
    document.body.appendChild(d);

    var i = document.getElementById(n);
    if (c && typeof(c.onComplete) == 'function') {
      i.onComplete = c.onComplete;
    }

    return n;
  },

  form : function(f, name) {
    f.setAttribute('target', name);
  },

  submit : function(f, c) {
    AIM.form(f, AIM.frame(c));
    if (c && typeof(c.onStart) == 'function') {
      return c.onStart();
    } else {
      return true;
    }
  },

  loaded : function(id) {
    var i = document.getElementById(id);
    if (i.contentDocument) {
      var d = i.contentDocument;
    } else if (i.contentWindow) {
      var d = i.contentWindow.document;
    } else {
      var d = window.frames[id].document;
    }
    if (d.location.href == "about:blank") {
      return;
    }

    if (typeof(i.onComplete) == 'function') {
      i.onComplete(d.body.innerHTML);
    }
  }

}
