
/* START mvc.js */
var Controller = new Object()

Controller.Methods = {
  run: function()
  {
    if (document.body.id) Controller.run_controller(document.body.id)
    Controller.run_controller('Application')
  },
  
  run_controller: function(id)
  {
    var controller_class = id + 'Controller'
    if (window[controller_class]) eval(controller_class + '.run()')
  }
}

Object.extend(Controller, Controller.Methods)

function init()
{
  // quit if this function has already been called
  if (arguments.callee.done) return

  // flag this function so we don't do the same thing twice
  arguments.callee.done = true

  // kill the timer
  if (_timer) clearInterval(_timer)

  // do stuff
  Controller.run()
}

/* for Mozilla/Opera9 */
if (document.addEventListener)
{
  document.addEventListener("DOMContentLoaded", init, false)
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>")
  var script = document.getElementById("__ie_onload")
  
  script.onreadystatechange = function()
  {
    if (this.readyState == "complete")
    {
      init() // call the onload handler
    }
  }
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent))
{
  var _timer = setInterval(function()
  {
    if (/loaded|complete/.test(document.readyState))
    {
      init() // call the onload handler
    }
  }, 10)
}

/* for other browsers */
window.onload = init


/* END mvc.js */

/* START toggler.js */
Toggler = Class.create()
Toggler.prototype = {
  initialize: function(control_id, target_id, save_callback)
  {
    this.control = $(control_id)
    this.target = $(target_id)
    
    this.state = this.target.visible()
    
    this.change_classes = true
    this.cancel_text = 'Close'
    this.original_text = this.control.innerHTML
    
    if (save_callback) this.save_callback = save_callback.bind(this)
    
    this.bound_event = this.on_click.bindAsEventListener(this)
  },
  
  show: function()
  {
    this.state = true
    this.control.innerHTML = this.cancel_text
    
    if (this.change_classes)
    {
      this.control.removeClassName('create')
      this.control.addClassName('destroy')
    }
    
    if (!this.control.hasClassName('noeffect'))
    {
      new Effect.Appear(this.target, {duration: 0.3})
    }
    else
    {
      this.target.show()
    }
    
    setTimeout(this.select_first_field.bind(this), 500)
    if (this.save_callback) this.save_callback()
  },
  
  hide: function()
  {
    this.state = false
    this.control.innerHTML = this.original_text
    
    if (this.change_classes)
    {
      this.control.addClassName('create')
      this.control.removeClassName('destroy')
    }
    this.target.hide()
    if (this.save_callback) this.save_callback()
  },
  
  select_first_field: function()
  {
    if (this.target.nodeName == 'FORM')
    {
      this.target.focusFirstElement()
      return true
    }
    else
    {
      try
      {
        var forms = $$('#' + this.target.id + ' form')
        if (forms.length > 0)
        {
          forms.first().focusFirstElement()
          return true
        }
      }
      catch (exception)
      {
        // The toggler target might not contain a form
        return false
      }
      
      return false
    }
  },
  
  toggle: function()
  {
    this.state ? this.hide() : this.show()
  },
  
  add_event: function()
  {
    Event.observe(this.control, 'click', this.bound_event)
  },
  
  remove_event: function()
  {
    Event.stopObserving(this.control, 'click', this.bound_event)
  },
  
  on_click: function(e)
  {
    this.toggle()
    Event.stop(e)
    return false
  }
}

/* END toggler.js */

/* START textarea_extensions.js */
TextAreaExtensions = Class.create()
TextAreaExtensions.prototype = {
  initialize: function()
  {
    this._add_images()
    
    this.bigger = $$('.bigger')
    this.smaller = $$('.smaller')
  
    // Add event observer to all more buttons
    this.bigger.each(function(item)
    {
      Event.observe(item, 'click', this.increase_rows.bindAsEventListener(this))
    }.bind(this))
  
    // Add event observer to all less buttons
    this.smaller.each(function(item)
    {
      Event.observe(item, 'click', this.decrease_rows.bindAsEventListener(this))
    }.bind(this))
  },

  increase_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    textarea.rows += 5
  },

  decrease_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    
    if (textarea.rows >= 5)
    {
      textarea.rows -= 4
    }
  },
  
  /** Private methods **/
  _get_next_textarea: function(element)
  {
    var children = $A(element.parentNode.parentNode.childNodes)

    return children.find(function(child) { return child.nodeName == 'TEXTAREA' })
  },
  
  _add_images: function()
  {
    $$('textarea').each(function(textarea)
    {
      new Insertion.Before(textarea, '<span class="resizer"><img class="bigger" alt="Increase the size of this text box" src="/images/box-open.png"/><img class="smaller" alt="Decrease the size of this text box" src="/images/box-close.png"/></span><br/>')
    })
  }
}

/* END textarea_extensions.js */

/* START select_search.js */
SelectSearch = Class.create()
SelectSearch.prototype = {
  initialize: function(search_input, search_results)
  {
    this.search_input = $(search_input)
    this.search_results = $(search_results)
    this.items = this.read_values()
    
    new Event.observe(this.search_input, 'click', this.clear_search_field.bindAsEventListener(this))
    new Event.observe(this.search_input, 'keydown', this.search.bindAsEventListener(this))
  },
  
  clear_search_field: function()
  {
    this.search_input.value = ''
    this.items.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  },
  
  read_values: function()
  {
    return $A(this.search_results.options).collect(function(option)
    {
      return {value: option.value, text: option.innerHTML}
    })
  },
  
  search: function()
  {
    var query = this.search_input.value
    var results = this.items.findAll(function(value)
    {
      return value.text.toLowerCase().match(query.toLowerCase())
    })
    
    this.search_results.options.length = 0
    results.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  }
}

/* END select_search.js */

/* START search.js */
var AutoClearField = new Class.create()
AutoClearField.prototype = {
  initialize: function(element)
  {
    this.add_events(element)
  },
  
  add_events: function(element)
  {
    this.field_value = element.value
    
    Event.observe(element, 'focus', function() { Field.clear(element); }.bind(this) )
    Event.observe(element, 'blur',  function() { element.value = this.field_value }.bind(this) )
  }
}

Ebi = Class.create();
Ebi.Autocompleter = Class.create();
Object.extend(Object.extend(Ebi.Autocompleter.prototype, Autocompleter.Base.prototype), {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  updateElement: function() {
  },

  getUpdatedChoices: function() {
    entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams) 
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }

});

/* Search class */
var SearchManager = new Class.create()
SearchManager.prototype = {
  initialize: function()
  {
    this.autocomplete_field = 'SearchResultsContainer'
    this.go_to_url = '/translations/edit/'
    this.search_url = '/search'
    this.id_replace = 'Translation_'
    this.form_id = 'TranslationSearch'
    this.field_id = 'TranslationSearchField'
    this.field_value = 'Find'
  },
  
  submit_ajax_translation_search: function(e)
  {
    for (var i = 0; i < $(this.autocomplete_field).childNodes[0].childNodes.length; i++)
    {
      if ($(this.autocomplete_field).childNodes[0].childNodes[i].className == 'selected')
      {
        var translation_id = $(this.autocomplete_field).childNodes[0].childNodes[i].id.replace(this.id_replace, '')
        if (translation_id)
        {
          window.location = this.go_to_url + translation_id
        } 
      }
    }
    Event.stop(e)
  },
  
  bind_events: function()
  {
    if ($(this.form_id))
    {
      new Ebi.Autocompleter(this.field_id, this.autocomplete_field, this.search_url, {
        indicator: 'SearchSpinner', 
        afterUpdateElement: function(e) { this.submit_ajax_translation_search(e) }.bind(this),
        onShow: function(element, update)
        {
          if (!update.style.position || update.style.position=='absolute')
          {
            update.style.position = 'absolute'
            Position.clone(element, update, {
              setHeight: false,
              setWidth: false,
              setLeft: false,
              setRight: true,
              offsetTop: element.offsetHeight
            })
          }
          
          update.style.right = '0'
          Element.show(update)
        }
      })
      Event.observe(this.field_id, 'focus', function() { Field.clear(this.field_id); }.bind(this) )
      Event.observe(this.field_id, 'blur',  function() { $(this.field_id).value = this.field_value; KeyboardHelper.underline_accesskey(this.field_id); }.bind(this) )
      Event.observe(this.form_id, 'submit', function(e) { this.submit_ajax_translation_search(e) }.bind(this))
    }
  }
}

var BlockSearch = new Class.create()
BlockSearch.prototype = {
  initialize: function(search_control_id, search_source_class)
  {
    this.busy = false
    
    this.search_control_id = search_control_id
    this.search_source_class = search_source_class
    
    Event.observe(this.search_control_id, 'keyup', this.search.bind(this))
  },
  
  search: function()
  {
    var search_term = $(this.search_control_id).value
    
    if (this.busy) return
    
    this.busy = true

    $(this.search_control_id).up().getElementsBySelector('.' + this.search_source_class).each(function(element)
    {
      if (element.innerHTML.match(RegExp(search_term, 'i')))
      {
        element.show()
      }
      else
      {
        element.hide()
      }
    })
    
    this.busy = false
  },
  
  show_all: function()
  {
    $(this.search_control_id).up().getElementsBySelector('.' + this.search_source_class).each(function(element)
    {
      element.show()
    })
  }
}

/* END search.js */

/* START remote_form.js */
RemoteForm = Class.create()
RemoteForm.prototype = {
  initialize: function(form_id, controller_name, item_name, callback)
  {
    this.form_id = form_id
    this.controller_name = controller_name
    this.item_name = item_name
    this.callback = callback
    this.form_display_callback = false
    this.add_completed_callback = null
    this.method = 'post'
    this.enctype = ''
    this.ajax = false
    
    this.edit_url    = '/' + this.controller_name + '/edit/'
    this.new_url     = '/' + this.controller_name + '/new'
    this.create_url  = '/' + this.controller_name + '/create'
    this.update_url  = '/' + this.controller_name + '/update/'
    this.add_close_button = true
    this.form_visible = false
  },
  
  edit: function(e)
  {
    var element = Event.element(e)
    
    if (element.nodeName == 'IMG')
    {
      element = element.up('.edit_' + this.item_name)
    }
    
    if (!element) return
    
    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('edit_' + this.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      this.show_form(this.form_id, this.edit_url + item_id, this.update_url + item_id, this.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  add: function(e)
  {
    var element = Event.element(e)

    if (element.nodeName == 'IMG')
    {
      element = element.up('.add_' + this.item_name)
    }

    if (!element) return

    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('add_' + this.item_name))
    {
      if (this.add_callback) this.add_callback()
      this.show_form(this.form_id, this.new_url, this.create_url, this.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  destroy: function(e)
  {
    var element = Event.element(e)
    if ((element.nodeName == 'A' || element.nodeName == 'IMG') && element.hasClassName('destroy_' + this.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      if (confirm('Are you sure you want to delete that item?'))
      {
        window.location = '/' + this.controller_name + '/destroy/' + item_id
      }

      Event.stop(e)
      return false
    }
  },

  show_form: function(id, url, save_url, callback)
  {
    if (this.form_visible)
    {
      return
    }
    else
    {
      this.form_visible = true
    }
    
    Busybox.busy('loading')
    
    function close_form()
    {
      if ($(id))
      {
        $(id).remove()
        Fader.remove()
      }
    }
    
    function focus_field()
    {
      try
      {
        if ($(id)) $(id).focusFirstElement()
      }
      catch (exception)
      {
        
      }
    }

    var fields_id = id + 'FormFields'
    var cancel_id = 'Cancel' + id
    var editor_html = ''
    var image_format = EffectHelper.suitable_image_format()
    var enctype = this.enctype ? 'enctype="multipart/form-data"' : ''
    
    close_form()

    editor_html += '<form style="display: none"' + this.ajax_form_html(save_url, id) + ' method="' + this.method + '" ' + enctype + ' action="' + save_url + '" class="edit standard" id="' + id + '">'
    if (this.add_close_button) editor_html += '  <div id="' + cancel_id + '" style="position: absolute; left: -15px; top: -25px; cursor: pointer"><img alt="Cancel without saving" style="cursor: pointer" width="36" height="36" border="0" src="/images/close.' + image_format + '"/></div>'
    editor_html += '  <div id="' + fields_id + '"></div>'
    editor_html += '</form>'
    
    // Insert the editor HTML
    new Insertion.Top(document.body, editor_html)
    
    // Get the form fields from the server
    new Ajax.Updater($(fields_id), url, {
      evalScripts: true,
      method: 'get',
      onComplete: function()
      {
        WindowHelper.center(id)
        new Effect.Appear($(id), { duration: 0.3, afterFinish: function() { Fader.alter_selects('visible'); focus_field() } })
        Fader.fade()
        Busybox.done()
        
        if (this.ajax)
        {
          try
          {
            Event.observe(id, 'submit', this.ajax_submit.bindAsEventListener(this))
          }
          catch(e)
          {
            console.log(e)
          }
        }
        
        Event.observe(cancel_id, 'click', function(e) { close_form(id); this.form_visible = false; Event.stop(e); return false }.bind(this))

        if (this.callback) this.callback()
        if (callback) callback()
      }.bind(this)
    })
  },
  
  ajax_form_html: function()
  {
    if (this.ajax == false) return ''
    
    return ' onsubmit="return false" '
  },
  
  ajax_submit: function()
  {
    this.form_visible = false
    new Ajax.Request(this.create_url, { onSuccess: this.ajax_on_succes, postBody: Form.serialize(this.form_id), onFailure: this.ajax_on_failure })
    $(this.form_id).remove()
    Fader.remove()
    return false
  }
}
/* END remote_form.js */

/* START preview.js */
/* Translation preview methods */

var Preview = new Class.create()
Preview.prototype = {
  initialize: function()
  {
    this.font_sizes = new Array
    this.preferences_callbacks = new Array
    this.default_size = 12
    if ($('translation_user_translation_id')) this.translation_id = $('translation_user_translation_id').value
  },
  
  add_slider_to: function(element_id, for_id)
  {
    $(element_id).innerHTML = '<div id="track1" style="float: none; width:300px" class="slider"><div id="handle1" class="handle"> </div></div>'

    new Control.Slider('handle1', 'track1', {
      range: $R(10, 95),
      sliderValue: 95,
      onSlide:function(v){$('PreviewTranslation').style.width = v + '%'},
      onChange:function(v){$('PreviewTranslation').style.width = v + '%'}
    })
  },

  font_size_changer: function(element_id, for_id)
  {
    Event.observe($(element_id), 'click', function(e)
    {
      var words = element_id.split(/_/)
      $(for_id).style.fontSize = words[words.length - 1]
      Event.stop(e)
    })
  },
  
  add_preferences_callback: function(for_id, callback)
  {
    this.preferences_callbacks[for_id] = callback
  },
  
  add_font_inc: function(element_id, for_id, default_size)
  {
    this.font_sizes[for_id] = default_size
    
    Event.observe($(element_id), 'click', function(e)
    {
      if (this.font_sizes[for_id] < 148)
      {
        this.font_sizes[for_id]++
        $(for_id).style.fontSize = this.font_sizes[for_id] + 'px'
        if (this.preferences_callbacks[for_id]) { this.preferences_callbacks[for_id](this.font_sizes[for_id]); }
      }
    }.bind(this))
  },
  
  add_font_dec: function(element_id, for_id, default_size)
  {
    if (!this.font_sizes[for_id])
    {
      this.font_sizes[for_id] = default_size
    }
      
    Event.observe($(element_id), 'click', function(e)
    {
      if (this.font_sizes[for_id] > 8)
      {
        this.font_sizes[for_id]--
        $(for_id).style.fontSize = this.font_sizes[for_id] + 'px'
        if (this.preferences_callbacks[for_id]) { this.preferences_callbacks[for_id](this.font_sizes[for_id]); }
      }
    }.bind(this))
  },
  
  add_font_switcher: function(element_id, for_id)
  {
    Event.observe($(element_id), 'change', function(e)
    {
      Preferences.save_preview_font(element_id, $(element_id).value, this.translation_id)
      this.change_body_fonts(element_id, for_id)
    }.bind(this))
  },
  
  add_header_font_switcher: function(element_id, for_id)
  {
    Event.observe($(element_id), 'change', function(e)
    {
      Preferences.save_preview_font(element_id, $(element_id).value, this.translation_id)
      this.change_header_fonts(element_id, for_id)
    }.bind(this))
  },
  
  change_body_fonts: function(element_id, for_id)
  {
    if ($(element_id).value)
    {
      for (i = 0; i < $(for_id).childNodes.length; i++)
      {
        var element = $(for_id).childNodes[i]
        if (!element.nodeName.match(/^H[1-6]/) && element.style)
        {
          element.style.fontFamily = $(element_id).value
        }
      }
    }
  },
  
  change_header_fonts: function(element_id, for_id)
  {
    $A(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']).each(function(h) {
      $(for_id).getElementsBySelector(h).each(function(header) {
        header.style.fontFamily = $(element_id).value
      })
    })
  }
}

/* END preview.js */

/* START preferences.js */
/* User preferences */
var Preferences = new Object()

Preferences.Methods = {
  save_translation_font_size: function(size, user_translation_id)
  {
    new Ajax.Request('/preferences/translation_font/set_size', {method:'post', postBody:'size=' + size + '&user_translation_id=' + user_translation_id})
  },
  
  save_translation_translation_font_size: function(size, user_translation_id)
  {
    new Ajax.Request('/preferences/translation_font/set_size', {method:'post', postBody: 'translation=true&size=' + size + '&user_translation_id=' + user_translation_id})
  },
  
  save_preview_font_size: function(size, user_translation_id)
  {
    new Ajax.Request('/preferences/preview_font/set_size', {method:'post', postBody: 'size=' + size + '&user_translation_id=' + user_translation_id})
  },

  save_preview_font: function(type, font, user_translation_id)
  {
    new Ajax.Request('/preferences/preview_font/set_font', {method:'post', postBody:'type=' + type + '&font=' + font + '&user_translation_id=' + user_translation_id})
  },

  save_tag_colour: function(tag_id, colour)
  {
    new Ajax.Request('/preferences/tag/set_colour', {method:'post', postBody:'tag_id=' + tag_id + '&colour=' + colour})
  },

  save_dashboard_view: function(view)
  {
    new Ajax.Request('/preferences/dashboard/set_view', {method:'post', postBody:'view=' + view});
  }
}

Object.extend(Preferences, Preferences.Methods)

/* END preferences.js */

/* START popinfo.js */
/* Tooltip help */
var PopInfo = new Object()

/*
Examples:

PopInfo.display(element, 'Saved')

*/

PopInfo.Methods = {
  display: function(element, info, callback)
  {
    var delay = 2000
    var popup_element_id = element.id + '_popup'

    PopInfo.remove(popup_element_id)
    new Insertion.Before(element, '<div style="display: none" id="' + popup_element_id + '" class="popinfo_frame"><div class="popinfo_inner">' + info + '</div><div class="popinfo_image"></div></div>')
    
    if (callback) callback(element)
    
    new Effect.Appear(popup_element_id, { duration: 0.2 })
    
    setTimeout((function() { PopInfo.fade_popup(popup_element_id) }), delay)
  },
  
  fade_popup: function(popup_element_id)
  {
    try
    {
      new Effect.Fade(popup_element_id)
      setTimeout((function() { PopInfo.remove(popup_element_id) }).bind(this), 1000)
    }
    catch (exception)
    {
    }
  },
  
  remove: function(popup_element_id)
  {
    try
    {
      if ($(popup_element_id)) Element.remove(popup_element_id)
    }
    catch (exception)
    {
      
    }
  }
}

Object.extend(PopInfo, PopInfo.Methods)
/* END popinfo.js */

/* START menu.js */
var Menu = Class.create()
Menu.prototype = {
  initialize: function(control_id, menu_id, menu_item_class)
  {
    this.control_id = control_id
    this.menu_id = menu_id
    this.menu_item_class = menu_item_class
    
    this.add_menu_control()
  },
  
  add_menu_control: function()
  {
    Event.observe(this.control_id, 'click', function(e)
    {
      this.remove_menu_control_event = this.remove_menu_control.bindAsEventListener(this)
      Event.observe(document, 'click', this.remove_menu_control_event)
      
      Element.toggle(this.menu_id)
      Position.absolutize(this.menu_id)
      Position.clone(this.control_id, this.menu_id, { setLeft: true, setTop: true, setWidth: false, setHeight: false, offsetTop: 22 })

      if (this.callback)
      {
        this.callback()
      }

      Event.stop(e)
      return false
    }.bind(this))
    
    $$('.' + this.menu_item_class).each(function(link)
    {
      Event.observe(link, 'click', function(e)
      {
        Event.stopObserving(document, 'click', this.remove_menu_control_event)
        Element.hide(this.menu_id)
        Event.stop(e)
        return false
      }.bind(this))
    }.bind(this))
  },
  
  remove_menu_control: function(e)
  {
    var element = Event.element(e)
    if (!element.hasClassName(this.menu_item_class) && element.nodeName != 'INPUT')
    {
      this.close_menu()
    }
  },
  
  close_menu: function()
  {
    Element.hide(this.menu_id)
    Event.stopObserving(document, 'click', this.add_menu_control)
  }
}
/* END menu.js */

/* START lightbox.js */
Lightbox = Class.create()
Lightbox.prototype = {
  initialize: function(target)
  {
    this.target = target
    this.link = this.target.up('a')
    this.container_id = 'GraphLargeContainer'
    this.close_id = 'GraphClose'
    
    this.remove_event = this.remove.bindAsEventListener(this)
    Event.observe(this.target, 'click', this.display.bindAsEventListener(this))
    Event.observe(this.link, 'click',   this.display.bindAsEventListener(this))
  },
  
  remove: function(e)
  {
    Event.stopObserving($(this.close_id), 'click', this.remove_event)
    $(this.container_id).remove()
    Fader.remove()
    Event.stop(e)
    return false
  },
  
  display: function(e)
  {
    var image_format = EffectHelper.suitable_image_format()
    var image = document.createElement('img')
    
    if ($('Loading'))
    {
      Event.stop(e)
      return false
    }
    
    if ($(this.container_id)) $(this.container_id).remove()
    Busybox.busy('loading')
    new Insertion.Top(document.body, '<div id="' + this.container_id + '" class="popinfo_container" style="display: none"><div id="' + this.close_id + '" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/close.' + image_format + '"/></div></div>')
    
    Event.observe($(this.close_id), 'click', this.remove_event)
    image.onload = function()
    {
      $(this.container_id).appendChild(image)
      
      Busybox.done()
      WindowHelper.center(this.container_id)
      Fader.fade()
      new Effect.Appear($(this.container_id), {duration: 0.3})
      
      return false
    }.bind(this)

    image.src = this.link.href

    Event.stop(e)
    return false
  }
}

/* END lightbox.js */

/* START fader.js */
var Fader = new Object()

Fader.Methods = {
  active: function()
  {
    return $('Fader') ? true : false
  },
  
  set_height: function()
  {
    $('Fader').setStyle({height: WindowHelper.page_size().height + 'px'})
  },
  
  fade: function()
  {
    new Insertion.Top(document.body, '<div id="Fader" style="display: none; position: absolute; z-index: 120; width: 100%; height: 100%; top: 0; left: 0; background-color: #000000"></div>')
    Position.prepare()
    Fader.set_height()
    $('Fader').setOpacity(0)
    $('Fader').show(0)
    Fader.alter_selects('hidden')
    Fader.alter_overflows('hidden')
    new Effect.Opacity('Fader', {duration: 0, from: 0.7, to: 0.7})
  },
  
  alter_selects: function(visible, selector)
  {
    if (!(/MSIE/.test(navigator.userAgent) && !window.opera)) return
    if (!selector) selector = 'select'
    
    $$(selector).each(function(element)
    {
      element.setStyle({ visibility: visible })
    })
  },
  
  alter_overflows: function(overflow_setting)
  {
    $$('.overflow').each(function(element)
    {
      element.setStyle({ overflow: overflow_setting })
    })
  },
  
  remove: function()
  {
    if (!$('Fader')) return
    
    new Effect.Opacity('Fader', {duration: 0.2, from: 0.7, to: 0.0, afterFinish: function()
    {
      $('Fader').remove()
      Fader.alter_selects('visible')
      Fader.alter_overflows('auto')
    }})
  }
}

Event.observe(window, 'resize', function()
{
  if (!$('Fader')) return
  
  Fader.set_height()
})

Object.extend(Fader, Fader.Methods)
/* END fader.js */

/* START document_manager.js */
var TranslationManager = new Class.create()
TranslationManager.prototype = {
  initialize: function()
  {
    this.height_offset = 270

    this.last_element = null
    this.autosave_period = -1
    
    this.changed = false
    this.saving = false
    this.changed_during_save = false
    this.autosave_setting_fetch_countdown = 5
    this.unsaved_warning = 'You have unsaved changes.'
    
    // These data structures represent the fields of the translation
    this.fields = $H({translation_source: {characters: 0, watch_height: true},
                      translation_translation: {characters: 0, watch_height: true},
                      translation_title: {characters: 0, watch_height: false},
                      translation_version_preview: {characters: 0, watch_height: true},
                      tag_name: {characters: 0, watch_height: false}})
  },

  stop_autosave: function() { this.changed = false },
  enable_autosave: function() { this.changed = true },

  get_autosave_period_with_ajax: function()
  {
    // Try and get the autosave period
    new Ajax.Request('/translations/period', {onSuccess: function(response) { this.autosave_period = response.responseText; }.bind(this), asynchronous:true })
  },
  
  autosaver: function()
  {
    // Add an event to prevent autosave running during a manual save
    Event.observe('translation_save', 'click', function() { this.stop_autosave() }.bind(this) )
    
    if (this.autosave_period > 0)
    {
      new PeriodicalExecuter(this.autosave.bind(this), this.autosave_period)
    }
    else
    {
      if (this.autosave_setting_fetch_countdown > 0)
      {
        this.autosave_setting_fetch_countdown--
        setTimeout('translation_manager.autosaver()', 100)
      }
      else
      {
        return
      }
    }
  },
  
  insert_text: function(text)
  {
    var field = this.last_element || $('translation_source')
    var caret_position = KeyboardHelper.get_caret_position(field)
    var new_text = field.value.substring(0, caret_position) + text + field.value.substring(caret_position)
    field.value = new_text
    this.focus_field()
  },
  
  focus_field: function()
  {
    if (this.last_element)
    {
      this.last_element.focus()
    }
    else
    {
      $('translation_translation').focus()
    }
  },
  
  autosave: function()
  {
    // Include 'do_not_focus' to prevent the field from focussing
    // That's normally required because using accesskeys or the mouse changes the focus away from the writing areas
    if (this.changed)
    {
      this.ajax_save_effects()
      // Save
      new Ajax.Request('/translations/update', {method:'post', asynchronous:true, onComplete:function() { $('translation_save').disabled = false }, postBody:'do_not_focus=true&' + Form.serialize($('TranslationForm'))})
    } 
  },
  
  ajax_save_effects: function()
  {
    // Disable the save button
    $('translation_save').disabled = true
    
    // Hide the feedback
    this.hide_feedback('EditorStatus')
    $('EditorStatus').innerHTML = '<img style="border: none;" class="spinner" src="/images/spinner.gif" />'
  },
  
  resize_editors: function()
  {
    var size = this.get_translation_size()

    this.fields.keys().each(function(element)
    {
      if (this.fields.get(element).watch_height && $(element))
      {
        $(element).setStyle({height: (size.height - this.height_offset) + 'px'})
      }
    }.bind(this))
  },
  
  get_translation_size: function()
  {
    var myWidth = 0, myHeight = 0

    if (typeof(window.innerWidth) == 'number')
    {
      myWidth = window.innerWidth
      myHeight = window.innerHeight
    }
    else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight))
    {
      myWidth = document.documentElement.clientWidth
      myHeight = document.documentElement.clientHeight
    }
    else if (document.body && (document.body.clientWidth || document.body.clientHeight))
    {
      myWidth = document.body.clientWidth
      myHeight = document.body.clientHeight
    }
    
    return {height: myHeight, width: myWidth}
  },
  
  add_resizer: function()
  {
    var left = $$('td.source').first()
    var right  = $$('td.translation').first()
    new Draggable('Drag', {handle: 'Handle', constraint: 'horizontal', revert: true, onDrag: function(draggable, e)
    {
      try
      {
        var width = Event.pointerX(e)
        if (width > 260 && width < (translation_manager.get_translation_size()['width'] - 300))
        {
          left.setStyle({width: width + 'px'})
          right.setStyle({width: (translation_manager.get_translation_size()['width']) - width + 'px'})
        }
      }
      catch (exception)
      {
        console.log(exception)
      }
    }})
  },
  
  set_title: function(value)
  {
    $('translation_title').value = value
    $('HeaderTitle').innerHTML = '<a href="/">Ebiwrite:</a> Editing ' + value
  },
  
  set_character_length: function(element_id, length)
  {
    this.fields.get(element_id).characters = $(element_id).value.length
  },
  
  watch_editors: function()
  {
    // Watch editing in the source areas
    this.fields.keys().each(function(element)
    {
      if ($(element) && ($(element).nodeName == 'INPUT' || $(element).nodeName == 'TEXTAREA'))
      {
        // Get the initial field lengths
        this.fields.get(element).characters = $(element).value.length

        // Set up some observers to watch as the length changes
        Event.observe(element, 'keyup', function()
        {
          if ($(element).value.length != this.fields.get(element).characters
              && ($('EditorStatus').innerHTML.indexOf('Unsaved') == -1 || $('EditorStatus').style.display == 'none'))
          {
            if ($('translation_save') && $('translation_save').disabled)
            {
              this.changed_during_save = true
            }
            
            // Watch clicking away from the translation when unsaved
            this.enable_autosave()
            /* Event.observe(window, 'beforeunload', function(event) { return this.unsaved_warning }.bind(this)) */
            window.onbeforeunload = window.Event ? function(event) { return this.unsaved_warning; }.bind(this) : function() { return this.unsaved_warning; }.bind(this)
            
            this.hide_feedback('EditorStatus')
            $('EditorStatus').style.borderColor = 'red'
            this.display_feedback('EditorStatus', 'Unsaved', 'status_error')
            if ($('ForgetChanges'))
              Element.show('ForgetChanges')
          }
        }.bind(this))
      }
    }.bind(this))

    Event.observe('TranslationForm', 'submit', function()
    {
      if ($('ForgetChanges')) Element.hide('ForgetChanges')
    })
  },
  
  add_version_slider: function(current_translation_id)
  {
    var element_id = 'VersionSlider'
    var first_version = parseInt($('first_version').innerHTML)
    var last_version = parseInt($('last_version').innerHTML)
    
    $(element_id).innerHTML = '<div id="track1" style="float: none; width:100%" class="slider"><div id="handle1" class="handle"> </div></div>'

    new Control.Slider('handle1', 'track1', {
      range: $R(first_version, last_version),
      sliderValue: last_version,
      values: $R(first_version, last_version),
      increment: 1,
      onSlide: function(v) { v = Math.round(v); $('current_version').innerHTML = v },
      onChange: function(v)
      {
        v = Math.round(v)
        if (v > 0 && v < last_version + 1)
        {
          $('current_version').innerHTML = v
          new Ajax.Request('/translations/version/' + current_translation_id,
          {
            postBody: '&version=' + v,
            onComplete: function(transport)
            {
              $('translation_version_preview').value = transport.responseText
            }
          })
        }
      }
    })
  },
  
  version_switch: function()
  {
    var current_translation_id = $('CurrentTranslationID').innerHTML ? $('CurrentTranslationID').innerHTML : 'new'
    var current_version = parseInt($('current_version').innerHTML)
    
    this.display_feedback('EditorStatus', 'Changing version', 'status_notice')
    
    new Ajax.Request('/translations/version_switch/' + current_translation_id, { evalScripts: true, postBody: '&version=' + current_version, onSuccess: function(transport)
    {
      $('translation_translation').value = transport.responseText
      new Ajax.Updater('Versions', '/translations/versions/' + current_translation_id, { evalScripts: true, onComplete: function() 
      {
        this.add_version_slider(current_translation_id);
        this.resize_editors();
        this.display_feedback('EditorStatus', 'Version switched', 'status_notice')
      }.bind(this) })
    }.bind(this)})
  },
  
  versions: function()
  {
    var current_translation_id = $('CurrentTranslationID').innerHTML ? $('CurrentTranslationID').innerHTML : 'new'
    
    $('translation_edit').show()
    $('translation_versions').hide()
    $('translation_preview').show()
    $('translation_version_switch').show()
    $('Preview').hide()
    $('Versions').show()
    $('Translation').hide()
    if ($('EditorStatus')) this.hide_feedback('EditorStatus')
    new Ajax.Updater('Versions', '/translations/versions/' + current_translation_id, { evalScripts: true, onComplete: function() { this.add_version_slider(current_translation_id); this.resize_editors() }.bind(this) })
  },
  
  edit: function()
  {
    $('translation_edit').hide()
    $('translation_preview').show()
    
    if ($('translation_versions'))
    {
      $('translation_versions').show()
      $('translation_version_switch').hide()
      $('Versions').hide()
    }
    
    $('Preview').hide()
    $('Translation').show()
    
    this.focus_field()
  },
  
  preview: function()
  {
    var current_translation_id = $('CurrentTranslationID').innerHTML ? $('CurrentTranslationID').innerHTML : 'new'
    
    $('translation_edit').show()
    $('translation_preview').hide()
    
    if ($('translation_versions'))
    {
      $('translation_versions').show()
      $('translation_version_switch').hide()
      $('Versions').hide()
    }
    
    $('Translation').hide()
    $('Preview').show()

    new Ajax.Updater('PreviewBody', '/translations/preview/' + current_translation_id, { evalScripts: true, parameters: $('translation_translation').serialize() })
  },

  forget_changes_observer: function(id, message, always_warn)
  {
    var element = (typeof(id) == 'String') ? $(id) : id
    
    event_type = element.nodeName == 'FORM' ? 'submit' : 'click'
    
    Event.observe(element, event_type, function(e)
    {
      if ((!always_warn && this.changed) || always_warn)
      {
        if (confirm(message))
        {
          window.onbeforeunload = null
        }
        else
        {     
          Event.stop(e)
          return false
        }
      }
      else
      {
        window.onbeforeunload = null
      }
    }.bind(this))
  },

  show_unsaved_warning: function(e) { return this.unsaved_warning; },
  fade_feedback: function() { if ($('Notice')) { setTimeout("var fade = new Effect.Fade('Notice', {});", 100 * 60); } },
  hide_feedback: function(element) { $(element).innerHTML = ''; },
  
  display_feedback: function(element, message, className)
  {
    $(element).style.display = 'block'
    $(element).innerHTML = '<span class="' + className + '">' + message + '</span>'
  }
}

/* END document_manager.js */

/* START contextual_help.js */
ContextualHelp = Class.create()
ContextualHelp.prototype = {
  initialize: function()
  {
    Event.observe(document, 'click', function(e)
    {
      var element = $(Event.element(e))
      
      if (!element.hasClassName('help')) return
      if (element.nodeName != 'A') return
      
      this.display_help(element.href)
      
      Event.stop(e)
      return false
    }.bind(this))
  },
  
  display_help: function(url)
  {
    var image_format = EffectHelper.suitable_image_format()
    var faded = Fader.active()
    
    if ($('Loading')) return
    
    if ($('HelpContainer')) $('HelpContainer').remove()
    
    Busybox.busy('loading')
    new Insertion.Top(document.body, '<div id="HelpContainer" class="popinfo_container" style="display: none"><div id="HelpClose" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/icons/close.' + image_format + '"/></div><div id="HelpContent"></div></div>')
    
    Event.observe($('HelpClose'), 'click', function() { $('HelpContainer').remove(); if (!faded) { Fader.remove() } })
    
    new Ajax.Updater('HelpContent', url, { insertion: Insertion.Bottom, onComplete: function()
    {
      Busybox.done()
      WindowHelper.center('HelpContainer')
      if (!faded) Fader.fade()
      new Effect.Appear($('HelpContainer'), {duration: 0.3})
    }})
  }
}
/* END contextual_help.js */

/* START colour_slider.js */
var ColourSlider = Class.create()
ColourSlider.prototype = {
  initialize: function(slider, colour_name, block, save_callback)
  {
    this.colour_block = block
    this.slider = slider
    this.colour_name = colour_name
    this.save_callback = save_callback
    
    this.track = slider
    this.handle = slider.down('div.handle')
    
    this.create_slider()
  },

  create_slider: function()
  {
    if (this.control) return this.control
    
    this.control = new Control.Slider(this.handle, this.track,
    {
      range: $R(0, 255),
      sliderValue: this.colour_block.get(this.colour_name),
      disabled: false,
      onChange: this.updateColourAndSave.bind(this),
      onSlide: this.updateColour.bind(this)
    })
  },
  
  updateColourAndSave: function(value)
  {
    this.updateColour(value)
    if (this.save_callback) this.save_callback()
  },
  
  updateColour: function(value)
  {
    this.colour_block.set(this.colour_name, Math.round(value))
    this.colour_block
    this.colour_block.update()
  }
}

/* END colour_slider.js */

/* START colour_block.js */
var ColourBlock = Class.create()
ColourBlock.prototype = {
  initialize: function(block, value_field)
  {
    this.colours = Array(0, 0, 0)
    this.block = block
    this.value_field = value_field
    
    this.set_preset_colours()
  },

  /* The following methods return and expect RGB values */
  red: function() { return this.colours[0] },
  green: function() { return this.colours[1] },
  blue: function() { return this.colours[2] },
  
  set_red: function(red) { this.colours[0] = red },
  set_green: function(green) { this.colours[1] = green },
  set_blue: function(blue) { this.colours[2] = blue },
  
  update: function()
  {
    var colour = 'rgb(' + this.colours[0] + ',' + this.colours[1] + ',' + this.colours[2] + ')'
    this.value_field.value = '#' + this.to_hex()
    $(this.block).setStyle({ backgroundColor: colour })
  },
  
  set_preset_colours: function()
  {
    try
    {
      var red   = $(this.value_field).value.charAt(1) + $(this.value_field).value.charAt(2)
      var green = $(this.value_field).value.charAt(3) + $(this.value_field).value.charAt(4)
      var blue  = $(this.value_field).value.charAt(5) + $(this.value_field).value.charAt(6)
      
      this.block.style.backgroundColor = this.value_field.value

      this.set_red(this.rgb(red))
      this.set_green(this.rgb(green))
      this.set_blue(this.rgb(blue))
      
      this.update()
    }
    catch (exception)
    {
      this.set_red(0)
      this.set_green(0)
      this.set_blue(0)
    }
  },
  
  to_hex: function()
  {
    var hex = ''
    
    $A(this.colours).each(function(colour)
    {
      var value = this.hex(colour)
      
      if (value.length == 1) { value = '0' + value }
      
      hex = hex + value
    }.bind(this))
    
    return hex
  },
  
  hex: function(d)
  {
    var hex_map = '0123456789ABCDEF'
    var h = hex_map.substr(d&15, 1)
    
    while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h }
    return h
  },
  
  rgb: function(hex)
  {
    return parseInt(hex, 16) || 0
  },
  
  /* Consider these private */
  get: function(colour_name)
  {
    switch(colour_name)
    {
      case 'red':
        return this.colours[0]
      break;
      
      case 'green':
        return this.colours[1]
      break;

      case 'blue':
        return this.colours[2]
      break;
    }
  },

  set: function(colour_name, value)
  {
    switch(colour_name)
    {
      case 'red':
        return this.set_red(value)
      break;
      
      case 'green':
        return this.set_green(value)
      break;

      case 'blue':
        return this.set_blue(value)
      break;
    }
  }
}

/* END colour_block.js */

/* START busybox.js */
var Busybox = new Object()

Busybox.Methods = {
  busy: function(operation)
  {
    var image_format = EffectHelper.suitable_image_format()

    new Insertion.Top(document.body, '<div id="Loading" style="display: none; width: 138px; height: 81px"><img width="138" height="81" src="/images/icons/' + operation + '.' + image_format + '" /></div>')
    WindowHelper.center('Loading')
    $('Loading').show()
  },
  
  remove: function()
  {
    if ($('Loading')) $('Loading').remove()
  },
  
  done: function()
  {
    new Effect.Fade($('Loading'), { afterFinish: function() { Busybox.remove() }})
  }
}

Object.extend(Busybox, Busybox.Methods)
/* END busybox.js */

/* START window_helper.js */
var WindowHelper = new Object

WindowHelper.Methods = {
  size: function()
  {
    var width, height
    
    if (self.innerHeight)
    {
      width  = self.innerWidth
      height = self.innerHeight
    }
    else if (document.documentElement && document.documentElement.clientHeight)
    {
      // IE 6 Strict Mode
      width  = document.documentElement.clientWidth
      height = document.documentElement.clientHeight
    }
    else if (document.body)
    {
      // IE
      width  = document.body.clientWidth
      height = document.body.clientHeight
    }
    
    return {width: width, height: height}
  },
  
  page_size: function()
  {
    var x_scroll, y_scroll
    var width, height
    var window_size = WindowHelper.size()

    if (window.innerHeight && window.scrollMaxY)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = window.innerHeight + window.scrollMaxY
    }
    else if (document.body.scrollHeight > document.body.offsetHeight)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = document.body.scrollHeight
    }
    else
    {
      x_scroll = document.body.offsetWidth
      y_scroll = document.body.offsetHeight
    }

    width  = x_scroll < window_size.width  ? window_size.width  : x_scroll
    height = y_scroll < window_size.height ? window_size.height : y_scroll

    return {width: width, height: height}
  },
  
  center: function(element)
  {
    var options = Object.extend({update: false}, arguments[1] || {})
    element = $(element)

    Position.prepare()

    var offset_x = (Position.deltaX + Math.floor((WindowHelper.size().width - element.getDimensions().width) / 2))   || '0'
    var offset_y = (Position.deltaY + Math.floor((WindowHelper.size().height - element.getDimensions().height) / 2)) || '0'

    element.setStyle({ left: offset_x + 'px' })
    element.setStyle({ top:  offset_y + 'px' })

    if (options.update)
    {
      Event.observe(window, 'resize', function() { Position.center(element) })
      Event.observe(window, 'scroll', function() { Position.center(element) })
    }
  }
}

Object.extend(WindowHelper, WindowHelper.Methods)
/* END window_helper.js */

/* START table_helper.js */
var TableHelper = new Object()

TableHelper.Methods = {
  stripe: function(class_name)
  {
    $$('.' + class_name).each(function(table)
    {
      $A(table.getElementsByTagName('tbody')).each(function(tbody)
      {
        var even = false
        $A(tbody.getElementsByTagName('tr')).each(function(tr, row_id)
        {
          $A(tr.getElementsByTagName('td')).each(function(td)
          {
            if (td.className.length == 0)
            {
              td.className = even ? 'even' : 'odd'
            }
          })
          even = !even
        })
      })
    })
  },

  /* Table sort using Ajax */
  table_sort: function(class_name, callback)
  {
    $$('.' + class_name).each(function(link)
    {
      Event.observe(link, 'click', function(e)
      {
        var link = Event.element(e)
        var sort = link.innerHTML.toLowerCase()
        var action = $('TagSearch') ? 'tag_sort' : 'sort'
        var params = $('TagSearch') ? '/' + window.location.pathname.split('/').last() : ''
        var url_base = ''

        if ($('Translations'))
        {
          url_base = '/translations/'
        }
        else if ($('Files'))
        {
          url_base = '/files/'
        }
        else if ($('Words'))
        {
          url_base = '/words/'
        }
        else if ($('Dictionaries'))
        {
          var dictionary_id = $$('.dictionary_selector_field').first().value
          
          if (dictionary_id)
          {
            url_base = '/dictionaries/' + dictionary_id + '/'
          }
          else
          {
            return false
          }
        }

        switch (sort)
        {
          case 'created':
            sort = 'created_on'
          break

          case 'last edited':
            sort = 'updated_on'
          break
        }

        // Request the list sorted 
        new Ajax.Updater('TranslationList', url_base + action +  '/' + sort + params,
        { onLoading: function() 
          { 
            if ($('SortSpinner'))
            {
              Element.show('SortSpinner')
            }
          },
          onComplete: function()
          { 
            TableHelper.stripe('standard')
            TableHelper.table_sort('table_sort', callback)
            if ($('SortSpinner'))
            {
              Element.hide('SortSpinner')
            }
            
            if (callback) callback.call()
            
            new Ajax.Updater('SortViewControl', url_base + 'sort_direction')
          }
        })

        Event.stop(e)
        return false
      })
    })
  },

  stripe_and_sort: function(callback)
  {
    TableHelper.stripe('standard')
    TableHelper.table_sort('table_sort', callback)
    TableHelper.table_sort('menu_sort', callback)
  }
}

Object.extend(TableHelper, TableHelper.Methods)
/* END table_helper.js */

/* START show_hide.js */
var ShowHideHelper = new Object()

ShowHideHelper.Methods = {
  remove_events: function()
  {
    $$('a.show_hide').each(function(element)
    {
      var toggler = new Toggler(element.id, element.id.replace(/Link$/, ''))
      toggler.remove_event()
    })
  },
  
  add_events: function()
  {
    $$('a.show_hide').each(function(element)
    {
      var toggler = new Toggler(element.id, element.id.replace(/Link$/, ''))
      toggler.add_event()
    })
  },
  
  invert: function(element)
  {
    var toggler = new Toggler(element.id, element.id.replace(/Link$/, ''))
    toggler.hide()
  },
  
  invert_and_set_title: function(element, title)
  {
    var toggler = new Toggler(element.id, element.id.replace(/Link$/, ''))
    toggler.hide()
    toggler.original_text = title
    element.innerHTML = title
  },
  
  sheet_list_show_hide: function()
  {
    $$('a.sheet_show_hide').each(function(element)
    {
      ShowHideHelper.add_sheet_show_hide(element)
    })
  },
  
  add_sheet_show_hide: function(element)
  {
    var toggler = new Toggler(element.id, element.id.replace(/Link$/, ''), function()
    {
      var view = this.state ? 'show' : 'hide'
      var sheet_id = this.target.id.match(/\d+$/)[0]
      
      Preferences.save_sheet_view_setting(view, sheet_id)
    })
    toggler.cancel_text = toggler.original_text
    toggler.add_event()
  },
  
  show_more_or_less: function(element, link)
  {
    var text = 'Less info'
    var element = $(element)
    
    if (link.innerHTML == 'Less info')
      text = 'More info'

    link.innerHTML = text
    var nodes = $$('#' + element.id + ' .advanced')
    if (!nodes || nodes.length == 0) return

    for (i = 0; i < nodes.length; i++)
    {
      if (nodes[i].className == 'advanced')
      {
        nodes[i].style.display = nodes[i].style.display == 'none' || !nodes[i].style.display  ? 'block' : 'none'
      }
    }
  }
}

Object.extend(ShowHideHelper, ShowHideHelper.Methods)
/* END show_hide.js */

/* START keyboard_helper.js */
var KeyboardHelper = new Object()

KeyboardHelper.Methods = {
  /* Text helpers */
  setCaretPosition: function(element, pos)
  {
    if (element.setSelectionRange)
    {
      element.focus()
      element.setSelectionRange(pos, pos)
    }
    else if (element.createTextRange)
    {
      var range = element.createTextRange()

      range.collapse(true)
      range.moveEnd('character', pos)
      range.moveStart('character', pos)
      range.select()
    }
  },
  
  get_caret_position: function(element)
  {
    if (element.setSelectionRange)
    {
      return element.selectionStart
    }
    else if (element.createTextRange)
    {
      // The current selection
      var range = document.selection.createRange()
      // We'll use this as a 'dummy'
      var stored_range = range.duplicate()
      // Select all text
      stored_range.moveToElementText(element)
      // Now move 'dummy' end point to end point of original range
      stored_range.setEndPoint('EndToEnd', range)

      return stored_range.text.length - range.text.length
    }
  },
  
  which_meta_key: function()
  {
    var key = ''
  
    if (navigator.platform.match(/win/i))
    {
      key = 'alt'
    }
    else if (navigator.platform.match(/mac/i))
    {
      key = 'ctrl'
    }
    else
    {
      key = 'ctrl or alt'
    }
  
    return key
  },

  underline_accesskey: function(element_id)
  {
    var element = $(element_id)
    var key = KeyboardHelper.which_meta_key()
  
    if (element.tagName == 'A')
    {
      re = new RegExp(element.accessKey, 'i')
      match = element.innerHTML.match(re)
      element.innerHTML = element.innerHTML.replace(re, '<span style="text-decoration: underline" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
    }
    else if (element.tagName == 'INPUT')
    {
      element.value = element.value + ' [' + key + '+' + String(element.accessKey).toLowerCase() + ']'
    }
    else if (element.tagName == 'BUTTON')
    {
      re = new RegExp(element.accessKey, 'i')
      match = element.innerHTML.match(re)
      element.innerHTML = element.innerHTML.replace(re, '<span style="text-decoration: underline" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
    }
  },

  underline_accesskeys: function()
  {
    var links = document.getElementsByTagName('a')
    var key = KeyboardHelper.which_meta_key()
  
    for (var i=0; i< links.length; i++)
    {
      if (links[i].accessKey.length > 0 && !links[i].innerHTML.match(/class=.?accesskey/))
      {
        re = new RegExp(links[i].accessKey, 'i')
        match = links[i].innerHTML.match(re)
        links[i].innerHTML = links[i].innerHTML.replace(re, '<span class="accesskey" style="text-decoration: underline" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
      }
    }
  
    KeyboardHelper.underline_form_accesskeys()
    KeyboardHelper.underline_form_button_accesskeys()
  },

  underline_form_button_accesskeys: function()
  {
    var inputs = document.getElementsByTagName('button')
    var key = KeyboardHelper.which_meta_key()

    for (var i = 0; i < inputs.length; i++)
    {
      if (inputs[i].accessKey.length > 0 && inputs[i].className.indexOf('nohelp') == -1)
      {
        re = new RegExp(inputs[i].accessKey, 'i')
        match = inputs[i].lastChild.nodeValue.match(re)

        var original_html = inputs[i].innerHTML.replace(inputs[i].lastChild.nodeValue, '')
        inputs[i].innerHTML = original_html + inputs[i].lastChild.nodeValue.replace(re, '<span style="text-decoration: underline" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
      }
    }
  },

  underline_form_accesskeys: function()
  {
    var inputs = document.getElementsByTagName('input')
    var key = KeyboardHelper.which_meta_key()

    for (var i = 0; i < inputs.length; i++)
    {
      if (inputs[i].accessKey.length > 0 && inputs[i].className.indexOf('nohelp') == -1)
      {
        inputs[i].value = inputs[i].value + ' [' + key + '+' + String(inputs[i].accessKey).toLowerCase() + ']'
      }
    }
  }
}

Object.extend(KeyboardHelper, KeyboardHelper.Methods)
/* END keyboard_helper.js */

/* START form_helper.js */
var FormHelper = new Object()

FormHelper.Methods = {
  disable_on_submit: function(tag_name)
  {
    $$('form').each(function(form)
    {
      var submit = $A(form.getElementsByTagName(tag_name)).find(function(input) { return input.type == 'submit' } )
      
      if (!submit) return
      
      Event.observe(form, 'submit', function(e)
      {
        if (submit && !submit.hasClassName('nodisable')) submit.disable()
      })
    })
  }
}

Object.extend(FormHelper, FormHelper.Methods)
/* END form_helper.js */

/* START effect_helper.js */
var EffectHelper = new Object()

EffectHelper.Methods = {
  lightbox: function()
  {
    $$('.lightbox').each(function(element)
    {
      new Lightbox(element)
    })
  },
  
  general_table_loading: function(id)
  {
    new Effect.Pulsate(id)
    if (ReportsController) ReportsController.update_general_graph(id)
  },
  
  suitable_image_format: function()
  {
    if (!navigator.appVersion.match(/MSIE/)) return 'png'
    
    try
    {
      var version = parseFloat(navigator.appVersion.split('MSIE')[1])
      return (version < 7.0) ? 'gif' : 'png'
    }
    catch (exception)
    {
      return 'png'
    }
  }
}

Object.extend(EffectHelper, EffectHelper.Methods)
/* END effect_helper.js */

/* START colour_block_helper.js */
if (!window.ColourBlockHelper) var ColourBlockHelper = new Object()

ColourBlockHelper.apply_colour_slider_to = function(slider, value_field, save_callback)
{
  var red = slider.down('div.slider')
  var green = red.next('div.slider')
  var blue = green.next('div.slider')
  var block = slider.next('div.colour_block')
  
  var colour_block = new ColourBlock(block, value_field)
  new ColourSlider(red, 'red', colour_block, save_callback)
  new ColourSlider(green, 'green', colour_block, save_callback)
  new ColourSlider(blue, 'blue', colour_block, save_callback)
}
/* END colour_block_helper.js */

/* START translations_controller.js */
// Variables
var translation_manager = new TranslationManager

var TranslationsController = new Object()
TranslationsController.Methods = {
  run: function()
  {
    if ($('word_search'))
    {
      this.add_dictionary_handling()
    }

    /* Dashboard controls */
    this.dashboard_controls()

    /* Striped tables */
    TableHelper.stripe_and_sort()

    if ($('SortViewControl'))
    {
      TranslationsController.sort_view_controls()
    }

    if ($('FileControl'))
    {
      var file_menu = new Menu('FileControl', 'FileSelector', 'file_name')
    }

    if ($('DictionaryControl'))
    {
      var dictionary_menu = new Menu('DictionaryControl', 'DictionarySelector', 'file_name')
      var remote_word_form = new RemoteForm('Word', 'words', 'word')

      remote_word_form.ajax = true
      remote_word_form.ajax_on_succes = TranslationsController.update_dictionary
      remote_word_form.add_close_button = false
      remote_word_form.add_callback = dictionary_menu.close_menu.bindAsEventListener(dictionary_menu)
      Event.observe($$('.add_word').first(), 'click', remote_word_form.add.bindAsEventListener(remote_word_form))
    }
    
    if ($('PreviewViewControl'))
    {
      TranslationsController.preview_view_controls()
    }

    if ($('StorageSlider'))
    {
      TranslationsController.storage_slider($('StorageSlider'))
    }

    /* IE exploder */
    if ($('translation_source'))
    {
      translation_manager.get_autosave_period_with_ajax()
      TranslationsController.last_element_manager()

      if ($('Handle'))
      {
        translation_manager.add_resizer()
      }
      
      TranslationsController.setup_autosave()
      translation_manager.focus_field()
    }

    if ($('New'))
    {
      // Remove the onunload event
      Event.observe($('TranslationForm'), 'submit', function ()
      {
        try
        {
          window.onbeforeunload = window.Event ? false : false
        }
        catch (exception)
        {
          // FIXME: Is the ternary expression correct there? IE excepts null
          window.onbeforeunload = null
        }
      })
    }

    // Add the save warning
    if ($('ForgetChanges'))
    {
      translation_manager.forget_changes_observer('ForgetChanges', $('New') ? 'Are you sure you want to discard this translation?' : 'Are you sure you want to discard your changes?', false)
    }

    // Add the delete warning
    if ($('Delete'))
    {
      $A(document.getElementsByTagName('FORM')).detect(function(element)
      {
        if (Element.childOf($('Delete'), element))
        {
          translation_manager.forget_changes_observer(element, 'Are you sure you want to delete this translation?', true)
          return true
        }
      })
    }

    if ($('EditorStatus'))
    {
      Event.observe($('EditorStatus'), 'click', function () { translation_manager.hide_feedback('EditorStatus') })
    }

    if ($('FormattingLink'))
    {
      Event.observe('FormattingLink', 'click', function(e)
      {
        if ($('Formatting').style.display == 'none')
        {
          $('Body').style.width = '68%'
          $('FormattingLink').innerHTML = 'hide formatting guide'
        }
        else
        {
          $('Body').style.width = '99%'
          $('FormattingLink').innerHTML = 'show formatting guide'
        }

        Element.toggle('Formatting')
        Event.stop(e)
      })
    }

    if ($('TagLinksControl'))
    {
      Element.hide('TagLinks')
      Event.observe('TagLinksControl', 'click', function(e)
      {
        if ($('TagLinks').style.display == 'none')
        {
          Element.hide('translation_tags')
          new Effect.Appear('TagLinks')
          $('TagLinksControl').innerHTML = 'show tag editor'
        }
        else
        {
          Element.hide('TagLinks')
          new Effect.Appear('translation_tags')
          $('TagLinksControl').innerHTML = 'view as links'
        }
        Event.stop(e)
      })
    }

    /* Set the default translation source font size */
    if ($('translation_source'))
    {
      var translation_id = $('translation_user_translation_id').value
      
      if (!preview)
      {
        var preview = new Preview
      }

      preview.add_preferences_callback('translation_source', function(size) { Preferences.save_translation_font_size(size, translation_id) } )
      preview.add_preferences_callback('translation_translation', function(size) { Preferences.save_translation_translation_font_size(size, translation_id) } )

      $$('div.font_control').each(function(font_control)
      {
        var resize_id = font_control.getElementsBySelector('.font_larger').first().title
        var default_size = $(resize_id).style.fontSize.replace('px', '') || 14

        preview.add_font_inc(font_control.getElementsBySelector('.font_larger').first(), resize_id, default_size)
        preview.add_font_dec(font_control.getElementsBySelector('.font_smaller').first(), resize_id, default_size)
      })
    }

    if ($('PreviewTranslation'))
    {
      TranslationsController.preview()
    }
  },
  
  preview: function()
  {
    var preview = new Preview
    var default_size = $('PreviewTranslation').style.fontSize.replace('px', '') || 14
    
    if ($('translation_user_translation_id'))
    {
      var translation_id = $('translation_user_translation_id').value
      if ($('PreviewContainer')) { preview.add_preferences_callback('PreviewTranslation', function(size) { Preferences.save_preview_font_size(size, translation_id) } ) }
    }
    
    preview.add_font_inc('FontBigger_Preview', 'PreviewTranslation', default_size)
    preview.add_font_dec('FontSmaller_Preview', 'PreviewTranslation', default_size)

    preview.add_font_switcher('BodyFont', 'PreviewTranslation')
    preview.add_header_font_switcher('HeaderFont', 'PreviewTranslation')
    
    preview.change_body_fonts('BodyFont', 'PreviewTranslation')
    preview.change_header_fonts('HeaderFont', 'PreviewTranslation')
  },
  
  setup_autosave: function()
  {
    translation_manager.fade_feedback()
    translation_manager.watch_editors()
    translation_manager.resize_editors()
    
    /* Removed due to IE issues: Event.observe(window, 'resize', translation_manager.resize_editors.bind(translation_manager)) */

    if (!$('New'))
    {
      translation_manager.autosaver()
    }
  },
  
  last_element_manager: function()
  {
    Event.observe(document, 'click', function(event)
    {
      var element = Event.element(event)
      switch (element.nodeName)
      {
        case 'INPUT':
        case 'TEXTAREA':
          if (element.id != 'word_search')
            translation_manager.last_element = element
        break
      }
    })
  },
  
  storage_slider: function(element_id)
  {
    $(element_id).innerHTML = '<div id="track1" style="margin: 4px 0 2px 0;width:250px;clear:none;background-color:#ccc;height:16px;"><div id="handle1" style="width:1px;height:16px;background-color:#f00;"> </div></div>'

    new Control.Slider('handle1', 'track1', {
      range: $R(0, 100),
      sliderValue: 0,
      disabled: true
    })

    $('handle1').style.width = ((parseInt($('SpaceUsed').innerHTML) * 2.5) + 1).toString() + 'px'
  },

  sort_view_controls: function()
  {
    new Menu('SortViewControl', 'SortViewOptions', 'menu_sort')
  },

  preview_view_controls: function()
  {
    var url_base = ''
    
    if ($('Translations'))
      url_base = '/dashboard_list/'
    else if ($('Files'))
      url_base = '/files/view/'
    else if ($('Words'))
      url_base = '/words/view/'
    
    Event.observe('PreviewViewControl', 'click', function(e)
    {
      new Ajax.Updater('TranslationList', url_base + 'preview', { onLoading: Element.show('SortSpinner'), onComplete: function() { TableHelper.stripe('standard'); TableHelper.table_sort('table_sort'); Element.hide('SortSpinner') }})
      $('PreviewViewControl').className = 'selected'
      $('ListViewControl').className = ''
      Event.stop(e)
      return false
    })
    
    Event.observe('ListViewControl', 'click', function(e)
    {
      new Ajax.Updater('TranslationList', url_base + 'list', { onLoading: Element.show('SortSpinner'),  onComplete: function() { TableHelper.stripe('standard'); TableHelper.table_sort('table_sort'); Element.hide('SortSpinner') }})
      $('ListViewControl').className = 'selected'
      $('PreviewViewControl').className = ''
      Event.stop(e)
      return false
    })
  },
  
  dashboard_controls: function()
  {
    if (!$('AddItemOptions')) return
    
    new Menu('AddItemControl', 'AddItemOptions', 'add_item')
    FilesController.dynamic_forms()
    DictionariesController.dynamic_forms()
    TranslationsController.dynamic_forms()
  },
  
  dynamic_forms: function()
  {
    Event.observe('AddTranslation', 'click', function()
    {
      window.location = $('AddTranslation').href
    })
  },
  
  remove_scrolling_file: function()
  {
    var container = $('translation_source').up()
    
    $('translation_source').remove()
    $('translation_source_url').value = ''
    new Insertion.Bottom(container, '<textarea accesskey="o" class="textinput" cols="40" id="translation_source" name="translation[source]" rows="20" style="font-size: 14px"></textarea>')
    new Ajax.Updater('translation_source', '/translations/source/' + $('CurrentTranslationID').innerHTML)
    translation_manager.resize_editors()
  },
  
  add_scrolling_file: function(file_id)
  {
    var container = $('translation_source').up()
    var url = '/attachments/display_source/' + file_id
    
    $('translation_source').remove()
    $('translation_source_url').value = url
    new Insertion.Bottom(container, '<div class="file_source overflow" id="translation_source"></div>')
    new Ajax.Updater('translation_source', url)
    translation_manager.resize_editors()
  },
  
  update_dictionary: function()
  {
    new Ajax.Updater('WordList', '/words', { method: 'get', onComplete: TranslationsController.add_dictionary_handling })
    
    translation_manager.display_feedback('EditorStatus', 'Dictionary updated', 'status_notice')
  },
  
  add_dictionary_handling: function()
  {
    new BlockSearch('word_search', 'file_name')
    new AutoClearField('word_search')
  },
  
  update_files: function()
  {
    new Ajax.Updater('FileList', '/attachments', { method: 'get' })
  }
}

Object.extend(TranslationsController, TranslationsController.Methods)
/* END translations_controller.js */

/* START tagsearch_controller.js */
colour_block = Array(0, 0, 0)

function create_colour_slider(id)
{
  function updateColourAndSave(value)
  {
    updateColour(value)
    Preferences.save_tag_colour($('SelectedTagId').innerHTML, colour_block)
  }

  function updateColour(value)
  {
    colour_block[id - 1] = Math.round(value)
    var colour = 'rgb(' + colour_block[0] + ',' + colour_block[1] + ',' + colour_block[2] + ')'
    $('ColourBlock').style.backgroundColor = colour
    $$('.tag_' + $('SelectedTagId').innerHTML).each(function(element)
    {
      $(element).style.color = colour
    })
  }

  new Control.Slider('colour_handle' + id, 'colour_track' + id,
  {
    range: $R(0, 255),
    sliderValue: colour_block[id -1],
    disabled: false,
    onChange: updateColourAndSave,
    onSlide: updateColour
  })
}

var TagSearchController = new Object()
TagSearchController.Methods = {
  run: function()
  {
    TableHelper.stripe_and_sort()
    TagSearchController.tag_editor()
    FilesController.dynamic_forms()
  },
  
  tag_editor: function()
  {
    /* Set the colour on the swatch */
    var current_tag = $$('.tag_' + $('SelectedTagId').innerHTML)
    if (current_tag.length > 0)
    {
      var colour = current_tag[0].style.color
      if (colour)
      {
        try
        {
          $('ColourBlock').style.backgroundColor = colour
          colour_block[0] = colour.split(',')[0].replace('rgb(', '')
          colour_block[1] = colour.split(',')[1]
          colour_block[2] = colour.split(',')[2].replace(')', '')
        }
        catch (exception)
        {
          // The colour has not been defined yet
        }
      }
    }
    
    /* Create the sliders */
    this.colour_sliders('TagEditor')
  },
  
  colour_sliders: function(element_id)
  {
    for (var i = 1; i < 4; i++)
      create_colour_slider(i)
    Element.toggle(element_id)
  }
}
Object.extend(TagSearchController, TagSearchController.Methods)
/* END tagsearch_controller.js */

/* START help_controller.js */
var HelpController = new Object()

HelpController.Methods = {
  run: function(id)
  {
    $('ToC').hide()
    
    Event.observe('ShowToC', 'click', function()
    {
      $('ToC').toggle()
    })
  }
}

Object.extend(HelpController, HelpController.Methods)
/* END help_controller.js */

/* START files_controller.js */
var FilesController = new Object()
FilesController.Methods = {
  run: function()
  {
    TableHelper.stripe_and_sort()

    if ($('SortViewControl'))
    {
      TranslationsController.sort_view_controls()
    }
    
    if ($('PreviewViewControl'))
    {
      TranslationsController.preview_view_controls()
    }
    
    TranslationsController.dashboard_controls()
  },

  tags_fix_callback: function()
  {
    $('tags_holder').show()
  },
  
  dynamic_forms: function()
  {
    if (!$('AddFile')) return
    
    var remote_form = new RemoteForm('File', 'attachments', 'file')
    remote_form.add_close_button = false
    remote_form.enctype = true
    /* Due to a bug in IE */
    remote_form.form_display_callback = this.tags_fix_callback
    Event.observe(document, 'click', remote_form.edit.bindAsEventListener(remote_form))
    Event.observe(document, 'click', remote_form.destroy.bindAsEventListener(remote_form))
    Event.observe($('AddFile'), 'click', remote_form.add.bindAsEventListener(remote_form))
  }
}

Object.extend(FilesController, FilesController.Methods)
/* END files_controller.js */

/* START dictionaries_controller.js */
var DictionariesController = new Object()
DictionariesController.Methods = {
  run: function()
  {
    if ($('SortViewControl')) this.sort_view_controls()
    
    TableHelper.stripe_and_sort(this.add_edit_in_place_controls)
    
    this.add_edit_in_place_controls()
    
    TranslationsController.dashboard_controls()
    
    this.remote_form = new RemoteForm('Dictionary', 'dictionaries', 'dictionary')
    this.remote_form.add_close_button = false

    Event.observe(document, 'click', this.remote_form.edit.bindAsEventListener(this.remote_form))
    Event.observe(document, 'click', this.remote_form.destroy.bindAsEventListener(this.remote_form))
    
    this.current_dicionary_id = $$('.dictionary_selector_field').first().value
    Event.observe($$('.dictionary_selector_field').first(), 'change', this.dictionary_selection.bindAsEventListener(this))
  },
  
  sort_view_controls: function()
  {
    TranslationsController.sort_view_controls(this.add_edit_in_place_controls)
  },
  
  add_edit_in_place_controls: function()
  {
    $$('.editable').each(function(element)
    {
      var matches = element.id.match(/([^_]*)_([^_]*)_([^_]*)/)
      var id = matches[3]
      var field = matches[2].toLowerCase()
      
      new Ajax.InPlaceEditor(element, '/words/update/' + id, { okText: 'Save', cancelText: 'Cancel', callback: function(form, value) { return '&word[' + field + ']=' + escape(value) } })
    })
  },
  
  dictionary_selection: function(event)
  {
    var element = Event.element(event)
    
    if (element.value == 'create')
    {
      this.remote_form.show_form(this.remote_form.form_id, this.remote_form.new_url, this.remote_form.create_url)
      element.value = this.current_dicionary_id
    }
    else if (element.value != this.current_dicionary_id)
    {
      window.location = '/dictionaries/' + element.value
    }
    
    this.current_dicionary_id = element.value
    Event.stop(event)
    return false
  },
  
  add_word_callback: function()
  {
    alert('Word added to the selected dictionary')
  },

  add_word_failed_callback: function(response)
  {
    alert(response.responseText)
  },

  dynamic_forms: function()
  {
    var remote_word_form = new RemoteForm('Word', 'words', 'word')
    remote_word_form.add_close_button = false
    remote_word_form.enctype = true
    remote_word_form.new_url = $('AddWord').href

    // Use Ajax to submit when not on the dictionary page
    if (!$('Dictionaries'))
    {
      remote_word_form.ajax_on_succes = DictionariesController.add_word_callback
      remote_word_form.ajax_on_failure = DictionariesController.add_word_failed_callback
      remote_word_form.ajax = true
    }

    Event.observe('AddWord', 'click', remote_word_form.add.bindAsEventListener(remote_word_form))
    Event.observe(document, 'click', remote_word_form.edit.bindAsEventListener(remote_word_form))
    Event.observe(document, 'click', remote_word_form.destroy.bindAsEventListener(remote_word_form))
  }
}

Object.extend(DictionariesController, DictionariesController.Methods)
/* END dictionaries_controller.js */

/* START book_controller.js */
var BookController = new Object()
BookController.Methods = {
  run: function()
  {
    TranslationsController.preview()
  }
}

Object.extend(BookController, BookController.Methods)
/* END book_controller.js */

/* START application_controller.js */
var ApplicationController = new Object()

ApplicationController.Methods = {
  run: function(id)
  {
    KeyboardHelper.underline_accesskeys()
    ApplicationController.search()
    new ContextualHelp
  },
  
  search: function()
  {
    ajax_search = new SearchManager
    
    if ($('TranslationSearch'))
    {
      ajax_search.bind_events()
    }
  }
}

Object.extend(ApplicationController, ApplicationController.Methods)
/* END application_controller.js */

/* START azukilib.js */
var Azuki = {
  Version: '0.0.1',
  Forms: { },
  Helpers: { },
  Storage: { },
  Style: { },
  Windowing: { }
}

/* START popinfo.js */

Azuki.Windowing.PopInfo = {
  display: function(element, info, callback)
  {
    var delay = 2000
    var popup_element_id = element.id + '_popup'

    this.remove(popup_element_id)
    new Insertion.Before(element, '<div style="display: none" id="' + popup_element_id + '" class="popinfo_frame"><div class="popinfo_inner">' + info + '</div><div class="popinfo_image"></div></div>')
    
    if (callback) callback(element)
    
    new Effect.Appear(popup_element_id, { duration: 0.2 })
    
    setTimeout((function() { this.fade_popup(popup_element_id) }.bind(this)), delay)
  },
  
  fade_popup: function(popup_element_id)
  {
    try
    {
      new Effect.Fade(popup_element_id)
      setTimeout((function() { this.remove(popup_element_id) }).bind(this), 1000)
    }
    catch (exception)
    {
    }
  },
  
  remove: function(popup_element_id)
  {
    try
    {
      if ($(popup_element_id)) Element.remove(popup_element_id)
    }
    catch (exception)
    {
      
    }
  }
}
/* END popinfo.js */

/* START lightbox.js */
Azuki.Windowing.Lightbox = Class.create()
Azuki.Windowing.Lightbox.prototype = {
  initialize: function(class_name)
  {
    this.class_name = typeof class_name == 'undefined' ? 'lightbox' : class_name
    
    this.container_id = 'LightboxContainer'
    this.close_id = 'LightboxClose'
    this.remote_type = 'image'
    
    this.remove_event = this.remove.bindAsEventListener(this)
    Event.observe(document, 'click', this.events.bind(this))
  },
  
  events: function(e)
  {
    var element = Event.element(e)
    this.image = null
    this.link = null
    
    if (element.nodeName == 'A' && element.down('img') && element.down('img').hasClassName(this.class_name))
    {
      this.image = element.down('img')
      this.link = element
    }
    
    if (element.nodeName == 'IMG' && element.hasClassName(this.class_name))
    {
      this.link = element.up('a')
      this.image = element
    }
    
    if (this.link && this.image)
    {
      this.display(e)
    }
  },
  
  remove: function(e)
  {
    Event.stopObserving($(this.close_id), 'click', this.remove_event)
    $(this.container_id).remove()
    Azuki.Windowing.Fader.remove()
    Event.stop(e)
    return false
  },
  
  display: function(e)
  {
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    
    if (Azuki.Windowing.Busybox.active())
    {
      Event.stop(e)
      return false
    }
    
    if ($(this.container_id)) $(this.container_id).remove()
    Azuki.Windowing.Busybox.busy('loading')
    new Insertion.Top(document.body, '<div id="' + this.container_id + '" class="popinfo_container" style="display: none"><div id="' + this.close_id + '" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/azuki/close.' + image_format + '"/></div></div>')

    Event.observe($(this.close_id), 'click', this.remove_event)

    switch(this.remote_type)
    {
      case 'image':
        var image = document.createElement('img')
        
        image.onload = function()
        {
          $(this.container_id).appendChild(image)
      
          Azuki.Windowing.Busybox.done()
          Azuki.Helpers.Window.center(this.container_id)
          Azuki.Windowing.Fader.fade()
          new Effect.Appear(this.container_id, {duration: 0.3})

          return false
        }.bind(this)

        image.src = this.link.href
      break
      
      case 'html':
        new Ajax.Updater(this.container_id, this.link.href, { method: 'get', evalScripts: true, onComplete: function()
        {
          Azuki.Windowing.Busybox.done()
          Azuki.Windowing.Fader.fade()
          Azuki.Helpers.Window.center($(this.container_id))
          
          $(this.container_id).show()
        }.bind(this)})
      break
    }
    
    if (e) Event.stop(e)
    return false
  }
}

/* END lightbox.js */

/* START fader.js */
Azuki.Windowing.Fader = {
  active: function()
  {
    return $('Fader') ? true : false
  },
  
  set_height: function()
  {
    $('Fader').setStyle({height: Azuki.Helpers.Window.page_size().height + 'px'})
  },
  
  fade: function()
  {
    new Insertion.Top(document.body, '<div id="Fader" style="display: none; position: absolute; z-index: 120; width: 100%; height: 100%; top: 0; left: 0; background-color: #000000"></div>')
    Position.prepare()
    Azuki.Windowing.Fader.set_height()
    $('Fader').setOpacity(0)
    $('Fader').show(0)
    Azuki.Windowing.Fader.alter_selects('hidden')
    Azuki.Windowing.Fader.alter_overflows('hidden')
    new Effect.Opacity('Fader', {duration: 0, from: 0.7, to: 0.7})
  },
  
  alter_selects: function(visible, selector)
  {
    if (!(/MSIE/.test(navigator.userAgent) && !window.opera)) return
    if (!selector) selector = 'select'
    
    $$(selector).each(function(element)
    {
      element.setStyle({ visibility: visible })
    })
  },
  
  alter_overflows: function(overflow_setting)
  {
    $$('.overflow').each(function(element)
    {
      element.setStyle({ overflow: overflow_setting })
    })
  },
  
  remove: function()
  {
    if (!$('Fader')) return
    
    new Effect.Opacity('Fader', {duration: 0.2, from: 0.7, to: 0.0, afterFinish: function()
    {
      $('Fader').remove()
      Azuki.Windowing.Fader.alter_selects('visible')
      Azuki.Windowing.Fader.alter_overflows('auto')
    }})
  }
}

Event.observe(window, 'resize', function()
{
  if (!$('Fader')) return
  
  Azuki.Windowing.Fader.set_height()
})

/* END fader.js */

/* START contextual_help.js */
Azuki.Windowing.ContextualHelp = Class.create()
Azuki.Windowing.ContextualHelp.prototype = {
  initialize: function(help_class)
  {
    this.help_class = help_class ? help_class : 'help'
    
    Event.observe(document, 'click', function(e)
    {
      var element = $(Event.element(e))
      if (!element.hasClassName(this.help_class)) return
      if (element.nodeName != 'A') return
      
      this.display_help(element.href)
      
      Event.stop(e)
      return false
    }.bind(this))
  },
  
  display_help: function(url)
  {
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var faded = Azuki.Windowing.Fader.active()

    if (Azuki.Windowing.Busybox.active()) return

    if ($('HelpContainer')) $('HelpContainer').remove()
    
    Azuki.Windowing.Busybox.busy('loading')
    new Insertion.Top(document.body, '<div id="HelpContainer" class="popinfo_container" style="display: none"><div id="HelpClose" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/azuki/close.' + image_format + '"/></div><div id="HelpContent"></div></div>')
    
    Event.observe($('HelpClose'), 'click', function() { $('HelpContainer').remove(); if (!faded) { Azuki.Windowing.Fader.remove() } })
    
    new Ajax.Updater('HelpContent', url, { insertion: Insertion.Bottom, onComplete: function()
    {
      Azuki.Windowing.Busybox.done()
      Azuki.Helpers.Window.center('HelpContainer')
      if (!faded) Azuki.Windowing.Fader.fade()
      new Effect.Appear($('HelpContainer'), {duration: 0.3})
    }})
  }
}
/* END contextual_help.js */

/* START busybox.js */
Azuki.Windowing.Busybox = {
  /* operation can be 'loading', 'saving', etc. depending on the images you've got. */
  busy: function(operation)
  {
    this.remove()
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()

    new Insertion.Top(document.body, '<div id="Busybox" style="display: none"><img width="138" height="81" src="/images/azuki/' + operation + '.' + image_format + '" /></div>')
    Azuki.Helpers.Window.center('Busybox')
    $('Busybox').show()
  },
  
  remove: function()
  {
    if ($('Busybox')) $('Busybox').remove()
  },
  
  done: function()
  {
    new Effect.Fade($('Busybox'), { afterFinish: function() { Azuki.Windowing.Busybox.remove() }})
  },
  
  active: function()
  {
    return $('Busybox') ? true : false
  }
}

/* END busybox.js */

/* START table_ruler.js */
/* Not implemented yet */
/* END table_ruler.js */

/* START color.js */
Azuki.Style.Color = {}
Azuki.Style.Color.RGB = Class.create()
Azuki.Style.Color.RGB.prototype = {
  initialize: function(value)
  {
    this.colors = Array(0, 0, 0)
    
    switch (typeof(value))
    {
      case 'string':
        this.set_from_rgb_string(value)
      break
      
      case 'object':
        this.colors = value
      break
    }
  },
  
  red: function() { return this.colors[0] },
  green: function() { return this.colors[1] },
  blue: function() { return this.colors[2] },

  set_red: function(red) { this.colors[0] = red },
  set_green: function(green) { this.colors[1] = green },
  set_blue: function(blue) { this.colors[2] = blue },

  /* Assumes rgb(1, 2, 3) */
  set_from_rgb_string: function(value)
  {
    this.colors = $A(value.replace(/rgb\(/, '').replace(/\)/, '').split(',')).collect(function(value)
    {
      return parseInt(value)
    })
  },

  to_s: function()
  {
    return 'rgb(' + this.red() + ',' + this.green() + ',' + this.blue() + ')'
  },

  to_hex: function()
  {
    var hex = ''
  
    $A(this.colors).each(function(colour)
    {
      var value = this._hex_value(colour)
    
      if (value.length == 1) { value = '0' + value }
    
      hex = hex + value
    }.bind(this))
  
    return hex
  },

  _hex_value: function(d)
  {
    var hex_map = '0123456789ABCDEF'
    var h = hex_map.substr(d&15, 1)
  
    while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h }
    return h
  }
}

Azuki.Style.Color.Hex = Class.create()
Azuki.Style.Color.Hex.prototype = {
  /* Create with values like this: '#000000' */
  initialize: function(value)
  {
    this.value = value
  },
  
  to_rgb: function()
  {
    this._set_rgb_values()
    var rgb_array = $A([this.red, this.green, this.blue]).collect(function(color)
    {
      return this._rgb_value(color)
    }.bind(this))
    
    return new Azuki.Style.Color.RGB(rgb_array)
  },
  
  _rgb_value: function(hex)
  {
    return parseInt(hex, 16) || 0
  },
  
  _set_rgb_values: function()
  {
    this.red   = this.value.charAt(1) + this.value.charAt(2)
    this.green = this.value.charAt(3) + this.value.charAt(4)
    this.blue  = this.value.charAt(5) + this.value.charAt(6)
  }
}

Azuki.Style.Color.Methods = {
  invert: function(value)
  {
    var color = new Azuki.Style.Color.RGB(value)
    color.set_red(255 - parseInt(color.red()))
    color.set_green(255 - parseInt(color.green()))
    color.set_blue(255 - parseInt(color.blue()))
    return color.to_s()
  },
  
  random: function()
  {
    var color = new Azuki.Style.Color.RGB(Array(Math.round((Math.random() * 255)), Math.round((Math.random() * 255)), Math.round((Math.random() * 255))))
    return color.to_s()
  }
}

Azuki.Style.Color.ElementMethods = {
  invertColor: function(element, property)
  {
    try
    {
      element.style[property] = Azuki.Style.Color.invert(element.getStyle(property))
      return true
    }
    catch (exception)
    {
      return false
    }
  },
  
  randomColor: function(element, property)
  {
    try
    {
      element.style[property] = Azuki.Style.Color.random()
      return true
    }
    catch (exception)
    {
      return false
    }
  }
}


Object.extend(Azuki.Style.Color, Azuki.Style.Color.Methods)
Element.addMethods(Azuki.Style.Color.ElementMethods)
/* END color.js */

/* START cookie.js */
Azuki.Storage.Cookie = {
  create: function(name, value, days, path)
  {
    var expires = ''
    
    path = typeof path == 'undefined' ? '/' : path
    
    if (days)
    {
      var date = new Date()
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000))
      expires = "; expires=" + date.toGMTString()
    }

    if (name && value)
    {
      document.cookie = name + '=' + escape(value) + expires + '; path=' + path
    }
  },
  
  find: function(name)
  {
    var matches = document.cookie.match(name + '=([^;]*)')

    if (matches && matches.length == 2)
    {
      return unescape(matches[1])
    }
  },
  
  destroy: function(name)
  {
    this.create(name, ' ', -1)
  }
}
/* END cookie.js */

/* START window_helper.js */
Azuki.Helpers.Window = {
  size: function()
  {
    var width, height
    
    if (self.innerHeight)
    {
      width  = self.innerWidth
      height = self.innerHeight
    }
    else if (document.documentElement && document.documentElement.clientHeight)
    {
      // IE 6 Strict Mode
      width  = document.documentElement.clientWidth
      height = document.documentElement.clientHeight
    }
    else if (document.body)
    {
      // IE
      width  = document.body.clientWidth
      height = document.body.clientHeight
    }
    
    return {width: width, height: height}
  },
  
  page_size: function()
  {
    var x_scroll, y_scroll
    var width, height
    var window_size = Azuki.Helpers.Window.size()

    if (window.innerHeight && window.scrollMaxY)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = window.innerHeight + window.scrollMaxY
    }
    else if (document.body.scrollHeight > document.body.offsetHeight)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = document.body.scrollHeight
    }
    else
    {
      x_scroll = document.body.offsetWidth
      y_scroll = document.body.offsetHeight
    }

    width  = x_scroll < window_size.width  ? window_size.width  : x_scroll
    height = y_scroll < window_size.height ? window_size.height : y_scroll

    return {width: width, height: height}
  },
  
  /* Centres absolute positions elements */
  center: function(element)
  {
    var options = Object.extend({update: false}, arguments[1] || {})
    element = $(element)

    Position.prepare()

    var offset_x = (Position.deltaX + Math.floor((Azuki.Helpers.Window.size().width - element.getDimensions().width) / 2))   || '0'
    var offset_y = (Position.deltaY + Math.floor((Azuki.Helpers.Window.size().height - element.getDimensions().height) / 2)) || '0'

    element.setStyle({ left: offset_x + 'px' })
    element.setStyle({ top:  offset_y + 'px' })

    if (options.update)
    {
      Event.observe(window, 'resize', function() { Position.center(element) })
      Event.observe(window, 'scroll', function() { Position.center(element) })
    }
  },
  
  center_with_margin: function(element)
  {
    var element = $(element)
    var container = element.up()
    var margin = (container.getWidth() - element.getWidth()) / 2
    element.setStyle({ marginLeft: margin + 'px', marginRight: margin + 'px'})
  }
}

/* END window_helper.js */

/* START keyboard_helper.js */
Azuki.Helpers.Keyboard = {
  which_meta_key: function()
  {
    var key = ''
  
    if (navigator.platform.match(/win/i))
    {
      key = 'alt'
    }
    else if (navigator.platform.match(/mac/i))
    {
      key = 'ctrl'
    }
    else
    {
      key = 'ctrl or alt'
    }
  
    return key
  },

  underline_accesskey: function(element)
  {
    var element = $(element)
    var key = this.which_meta_key()
    var regex = new RegExp(element.accessKey, 'i')
    var match = ''

    if (element.accessKey.length == 0 || element.hasClassName('noaccesskey') > 0) return

    if (element.tagName == 'A' && !element.innerHTML.match(/class=.?accesskey/))
    {
      match = element.innerHTML.match(regex)
      element.innerHTML = element.innerHTML.replace(regex, '<span class="accesskey" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
    }
    else if (element.tagName == 'INPUT')
    {
      element.value = element.value + ' [' + key + '+' + String(element.accessKey).toLowerCase() + ']'
    }
    else if (element.tagName == 'BUTTON')
    {
      var text = ''
      
      /* Remove text nodes from the button, this assumes the button has a format like: image text */
      $A(element.childNodes).each(function(child)
      {
        if (child.nodeName == '#text')
        {
          text = text + child.data
          element.removeChild(child)
        }
      })
      
      match = text.match(regex)
      new Insertion.Bottom(element, text.replace(regex, '<span class="accesskey" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>'))
    }
  },

  underline_accesskeys: function()
  {
    this.underline_tag_accesskeys('a')
    this.underline_tag_accesskeys('button')
    this.underline_tag_accesskeys('input')
  },

  underline_tag_accesskeys: function(tag_name)
  {
    var key = this.which_meta_key()
    $A(document.getElementsByTagName(tag_name)).each(function(element)
    {
      this.underline_accesskey(element)
    }.bind(this))
  }
}

/* END keyboard_helper.js */

/* START forms_helper.js */
Azuki.Helpers.Forms = {
  /* Text helpers */
  set_caret_position: function(element, pos)
  {
    if (element.setSelectionRange)
    {
      element.focus()
      element.setSelectionRange(pos, pos)
    }
    else if (element.createTextRange)
    {
      var range = element.createTextRange()

      range.collapse(true)
      range.moveEnd('character', pos)
      range.moveStart('character', pos)
      range.select()
    }
  },
  
  get_caret_position: function(element)
  {
    if (element.setSelectionRange)
    {
      return element.selectionStart
    }
    else if (element.createTextRange)
    {
      // The current selection
      var range = document.selection.createRange()
      // We'll use this as a 'dummy'
      var stored_range = range.duplicate()
      // Select all text
      stored_range.moveToElementText(element)
      // Now move 'dummy' end point to end point of original range
      stored_range.setEndPoint('EndToEnd', range)

      return stored_range.text.length - range.text.length
    }
  },

  activate_controls: function(names)
  {
    $A(document.getElementsByTagName('input')).each(function(element)
    {
      $A(names).each(function(name)
      {
        if (element.name == name)
        {
          element.disabled = false
        }
      })
    })
  },
  
  disable_on_submit: function()
  {
    $$('form').each(function(form)
    {
      var submit = $A(form.getElementsByTagName('input')).find(function(input) { return input.type == 'submit' } )
      
      if (!submit) return
      
      Event.observe(form, 'submit', function(e)
      {
        submit = $(submit)
        if (submit && !submit.hasClassName('nodisable')) submit.disable()
      })
    })
  }
}

/* END forms_helper.js */

/* START compatibility_helper.js */
Azuki.Helpers.Compatibility = {
  /* Rather than hacking IE's png transparency, we currently use this to switch between gif and png. */
  suitable_image_format: function()
  {
    if (!navigator.appVersion.match(/MSIE/)) return 'png'
    
    try
    {
      var version = parseFloat(navigator.appVersion.split('MSIE')[1])
      return (version < 7.0) ? 'gif' : 'png'
    }
    catch (exception)
    {
      return 'png'
    }
  }
}

/* END compatibility_helper.js */

/* START textarea_extensions.js */
Azuki.Forms.TextAreaExtensions = Class.create()
Azuki.Forms.TextAreaExtensions.prototype = {
  initialize: function()
  {
    this._add_images()
    
    this.bigger = $$('.bigger')
    this.smaller = $$('.smaller')
  
    // Add event observer to all more buttons
    this.bigger.each(function(item)
    {
      Event.observe(item, 'click', this.increase_rows.bindAsEventListener(this))
    }.bind(this))
  
    // Add event observer to all less buttons
    this.smaller.each(function(item)
    {
      Event.observe(item, 'click', this.decrease_rows.bindAsEventListener(this))
    }.bind(this))
  },

  increase_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    textarea.rows += 5
  },

  decrease_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    
    if (textarea.rows >= 5)
    {
      textarea.rows -= 4
    }
  },
  
  /** Private methods **/
  _get_next_textarea: function(element)
  {
    var children = $A(element.parentNode.parentNode.childNodes)

    return children.find(function(child) { return child.nodeName == 'TEXTAREA' })
  },
  
  _add_images: function()
  {
    $$('textarea').each(function(textarea)
    {
      new Insertion.Before(textarea, '<span class="resizer"><img class="bigger" alt="Increase the size of this text box" src="/images/azuki/open.png"/><img class="smaller" alt="Decrease the size of this text box" src="/images/azuki/close_small.png"/></span><br/>')
    })
  }
}

/* END textarea_extensions.js */

/* START select_search.js */
Azuki.Forms.SelectSearch = Class.create()
Azuki.Forms.SelectSearch.prototype = {
  initialize: function(search_input, search_results)
  {
    this.search_input = $(search_input)
    this.search_results = $(search_results)
    this.items = this.read_values()
    
    new Event.observe(this.search_input, 'click', this.clear_search_field.bindAsEventListener(this))
    new Event.observe(this.search_input, 'keydown', this.search.bindAsEventListener(this))
  },
  
  clear_search_field: function()
  {
    this.search_input.value = ''
    this.items.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  },
  
  read_values: function()
  {
    return $A(this.search_results.options).collect(function(option)
    {
      return {value: option.value, text: option.innerHTML}
    })
  },
  
  search: function()
  {
    var query = this.search_input.value
    var results = this.items.findAll(function(value)
    {
      return value.text.toLowerCase().match(query.toLowerCase())
    })
    
    this.search_results.options.length = 0
    results.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  }
}

/* END select_search.js */

/* START remote.js */

Azuki.Forms.Remote = Class.create()
Azuki.Forms.Remote.prototype = {
  initialize: function(options)
  {
    this.options = {
      form_id:                'RemoteForm',
      controller_name:        '',
      item_name:              '',
      method:                 'post',
      enctype:                null,
      ajax:                   false,
      add_close_button:       true,
      form_visible:           false,
      form_display_callback:  false,
      add_completed_callback: null
    }
    Object.extend(this.options, options || { })
    
    this.options.edit_url    = '/' + this.options.controller_name + '/edit/'
    this.options.new_url     = '/' + this.options.controller_name + '/new'
    this.options.create_url  = '/' + this.options.controller_name + '/create'
    this.options.update_url  = '/' + this.options.controller_name + '/update/'
    this.options.destroy_url = '/' + this.options.controller_name + '/destroy/'
  },
  
  edit: function(e)
  {
    var element = Event.element(e)
    
    if (element.nodeName == 'IMG')
    {
      element = element.up('.edit_' + this.options.item_name)
    }
    
    if (!element) return
    
    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('edit_' + this.options.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      this.show_form(this.options.form_id, this.options.edit_url + item_id, this.options.update_url + item_id, this.options.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  add: function(e)
  {
    var element = Event.element(e)

    if (element.nodeName == 'IMG')
    {
      element = element.up('.add_' + this.options.item_name)
    }

    if (!element) return

    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('add_' + this.options.item_name))
    {
      if (this.options.add_callback) this.options.add_callback()
      this.show_form(this.options.form_id, this.options.new_url, this.options.create_url, this.options.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  destroy: function(e)
  {
    var element = Event.element(e)
    if ((element.nodeName == 'A' || element.nodeName == 'IMG') && element.hasClassName('destroy_' + this.options.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      if (confirm('Are you sure you want to delete that item?'))
      {
        window.location = this.options.destroy_url + item_id
      }

      Event.stop(e)
      return false
    }
  },

  show_form: function(id, url, save_url, callback)
  {
    if (this.options.form_visible)
    {
      return
    }
    else
    {
      this.options.form_visible = true
    }
    
    Azuki.Windowing.Busybox.busy('loading')
    
    function close_form()
    {
      if ($(id))
      {
        $(id).remove()
        Azuki.Windowing.Fader.remove()
      }
    }
    
    function focus_field()
    {
      try
      {
        if ($(id)) $(id).focusFirstElement()
      }
      catch (exception)
      {
        
      }
    }

    var fields_id = id + 'FormFields'
    var cancel_id = 'Cancel' + id
    var editor_html = ''
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var enctype = typeof this.options.enctype == 'undefined' ? '' : 'enctype="multipart/form-data"'
    
    close_form()

    editor_html += '<form style="display: none"' + this.ajax_form_html(save_url, id) + ' method="' + this.options.method + '" ' + enctype + ' action="' + save_url + '" class="edit standard" id="' + id + '">'
    if (this.options.add_close_button) editor_html += '  <div id="' + cancel_id + '" style="position: absolute; left: -15px; top: -25px; cursor: pointer"><img alt="Cancel without saving" style="cursor: pointer" width="36" height="36" border="0" src="/images/close.' + image_format + '"/></div>'
    editor_html += '  <div id="' + fields_id + '"></div>'
    editor_html += '</form>'
    
    // Insert the editor HTML
    new Insertion.Top(document.body, editor_html)
    
    // Get the form fields from the server
    new Ajax.Updater($(fields_id), url, {
      evalScripts: true,
      method: 'get',
      onComplete: function()
      {
        Azuki.Helpers.Window.center(id)
        new Effect.Appear($(id), { duration: 0.3, afterFinish: function() { Azuki.Windowing.Fader.alter_selects('visible'); focus_field() } })
        Azuki.Windowing.Fader.fade()
        Azuki.Windowing.Busybox.done()
        
        if (this.options.ajax)
        {
          try
          {
            Event.observe(id, 'submit', this.ajax_submit.bindAsEventListener(this))
          }
          catch(e)
          {
            console.log(e)
          }
        }
        
        Event.observe(cancel_id, 'click', function(e) { close_form(id); this.options.form_visible = false; Event.stop(e); return false }.bind(this))

        if (callback) callback()
      }.bind(this)
    })
  },
  
  ajax_form_html: function()
  {
    if (this.options.ajax == false) return ''
    
    return ' onsubmit="return false" '
  },
  
  ajax_submit: function()
  {
    this.options.form_visible = false
    new Ajax.Request(this.options.create_url, { onSuccess: this.options.ajax_on_succes, postBody: Form.serialize(this.options.form_id), onFailure: this.options.ajax_on_failure })
    $(this.options.form_id).remove()
    Azuki.Windowing.Fader.remove()
    return false
  }
}
/* END remote.js */

/* START controller.js */
Azuki.Controller = {
  run: function()
  {
    if (document.body.id) Azuki.Controller.run_controller(document.body.id)
    Azuki.Controller.run_controller('Application')
  },
  
  run_controller: function(id)
  {
    var controller_class = id + 'Controller'
    
    if (!window[controller_class]) return

    if (eval(controller_class + '.run'))
    {
      eval(controller_class + '.run()')
    }
    else
    {
      controller = new window[controller_class]
    }
  }
}

function init()
{
  // quit if this function has already been called
  if (arguments.callee.done) return

  // flag this function so we don't do the same thing twice
  arguments.callee.done = true

  // kill the timer
  if (_timer) clearInterval(_timer)

  // do stuff
  Azuki.Controller.run()
}

/* for Mozilla/Opera9 */
if (document.addEventListener)
{
  document.addEventListener("DOMContentLoaded", init, false)
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  var void_src_url = location.protocol == "https:" ?  "https://javascript:void(0)" : "javascript:void(0)";
  var script = document.createElement("script");
  document.write("<script id=__ie_onload defer src='" + void_src_url + "'><\/script>")
  
  script.onreadystatechange = function()
  {
    if (this.readyState == "complete")
    {
      init() // call the onload handler
    }
  }
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent))
{
  var _timer = setInterval(function()
  {
    if (/loaded|complete/.test(document.readyState))
    {
      init() // call the onload handler
    }
  }, 10)
}

/* for other browsers */
window.onload = init


/* END controller.js */

/* END azukilib.js */
