Element.implement({
    getValues: function() {
        var formData = $H();

        // Get all input elements in the form
        this.getElements('input, select, textarea', true).each(function(el){
            // Ignore input elements that: do not have a name, are disabled, have the types submit, reset or file
            if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file')
                return;

            // Work out the value based on the type of input it is
            var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
                // Select elements should return the selected option
                return opt.value;
            }) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;

            // Convert the value into an array if it isn't already and store the values
            $splat(value).each(function(val){
              if (typeof val != 'undefined') {
                if (formData[el.name]) {
                  formData[el.name] = $splat(formData[el.name]);
                  formData[el.name].push(val);
                } else
                  formData[el.name] = val;
              }
            });
        });

        return formData;
    },

    getJSONValues: function(escape) {
        // Similar to getValues but removes PHP specific array identifiers []
        // so that it can be encoded into JSON objects
        var formData = $H();
        escape = (escape == true);

        // Get all input elements in the form
        this.getElements('input, select, textarea', true).each(function(el){
            // Ignore input elements that: do not have a name, are disabled, have the types submit, reset or file
            if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file')
                return;

            // Work out the value based on the type of input it is
            var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
                // Select elements should return the selected option
                return opt.value;
            }) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;

            // Convert the value into an array if it isn't already and store the values
            var iteration = 0;

            $splat(value).each(function(val){
                if (typeof val != 'undefined') {
                  if (escape) {
                    val = (encodeURIComponent) ? encodeURIComponent(val) : escape(val);
                    val = val.replace(/%22/g, '\"');
                  }

                  // If this is an array, treat it as such
                  if (el.name.match(/\[\]/g) == '[]') {
                      var name = el.name.replace(/\[\]$/,'');
                      formData[name] = $splat(formData[name]);
                      formData[name].push(val);
                  } else
                      formData[el.name] = val;
                }
            });
        });

        return formData;
    },

    setValues: function(data) {
        // Get elements with the class input
        $$('.input').each(function(el){
            // Ignore input elements that: do not have a name, are disabled, have the types submit, reset or file
            if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file' || el.type == 'hidden')
                return;

            if (el.type == 'text' || el.type == 'textarea') {
                el.set('value', data[el.id]);
            } else if (el.type == 'select-one') {
                for (var i=0; i<el.length; i++) {
                    if (el[i].value == data[el.id])
                        el.selectedIndex = i;
                }
            } else if (el.type == 'checkbox') {
                el.checked = (data[el.id]) ? true : false;
            }
        });
    },

    /*  Property: isVisible
        Returns a boolean; true = visible, false = not visible.

        Example:
        >$(id).isVisible()
        > > true | false    */
    isVisible: function() {
        return this.getStyle('display') != 'none';
    },

    setVisible: function(state, display_state) {
        if (state)
            this.show(display_state || '');
        else
            this.hide();
    },

     /*  Property: toggle
     *  Toggles the display of the element
     *
     *  Example:
     *  >$(id).toggle()
     */
    toggle: function(display) {
        if (this.isVisible())
            this.hide();
        else
            this.show(display);
    }

});

/**
 * Implement an analogue to PHP's substr_replace in JavaScript.
 */
String.implement({
    substrReplace: function(replacement, start, length) {
        // Default value for length is the replacement string length
        length = length || replacement.length;

        // Split the string either side of the replacement boundaries
        var before_string = start > 1 ? this.substr(0, start) : '';
        var after_string = this.substr(start + length);

        // Insert the replacement text and return the reassembled string
        return before_string + replacement + after_string;
    }
});


/**
 * The following implementations override the default Hash.toQueryString and
 * Object.toQueryString methods from Mootools with one that does NOT numerically
 * index arrays of values.
 *
 * This returns a query string of value[]=1&value[]=2... instead of the default
 * Mootools value[1]=1&value[2]=2..., which works better with PHP's $_POST array.
 */

Hash.implement({
  toQueryString: function(base){
    var queryString = [];
    Hash.each(this, function(value, key){
      if (base) key = base;
      var result;
      switch ($type(value)){
        case 'object': result = Hash.toQueryString(value, key); break;
        case 'array':
          var qs = {};
          value.each(function(val, i){
            qs[i] = val;
          });
          result = Hash.toQueryString(qs, key);
        break;
        default: result = key + '=' + encodeURIComponent(value);
      }
      if (typeof(value) != 'undefined') queryString.push(result);
    });

    return queryString.join('&');
  }
});

Object.extend({
    toQueryString: function(object, base){
        var queryString = [];

        Object.each(object, function(value, key){
            if (base) key = base;
            var result;
            switch (typeOf(value)){
                case 'object': result = Object.toQueryString(value, key); break;
                case 'array':
                    var qs = {};
                    value.each(function(val, i){
                        qs[i] = val;
                    });
                    result = Object.toQueryString(qs, key);
                break;
                default: result = key + '=' + encodeURIComponent(value);
            }
            if (value != null) queryString.push(result);
        });

        return queryString.join('&');
    }
});


/**
 * creates a Mock event to be used with fire event
 * @param Element target an element to set as the target of the event - not required
 *  @param string type the type of the event to be fired. Will not be used by IE - not required.
 *
 */
Event.Mock = function(target,type){
    var e = window.event;
    type = type || 'click';

    if (document.createEvent){
        e = document.createEvent('HTMLEvents');
        e.initEvent(
          type, //event type
          false, //bubbles - set to false because the event should like normal fireEvent
          true //cancelable
        );
    }
    e = new Event(e);
    e.target = target;
    return e;
}



/**
 * Extend Fx.Accordion with some useful methods
 */

Fx.Accordion.implement({
    isOpen: function(index) {
        index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
        return (this.elements[index].style.height == 'auto' || this.elements[index].style.height == '' || this.elements[index].getStyle('height').toInt() > 0);
    },

    isClosed: function(index) {
        index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
        return (this.elements[index].style.height != '' && this.elements[index].getStyle('height').toInt() == 0);
    },

    resize: function(index) {
        index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
        if (this.isOpen(index)) {
            var el = this.elements[index];
            el.setStyle('height', el.getFirst().getSize().y + 'px');
        }
    },

    show: function(index) {
        index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
        if (!this.isOpen(index))
            this.display(index);
    },

    close: function(index) {
        index = ($type(index) == 'element') ? this.elements.indexOf(index) : index;
        if (this.isOpen(index)) {
            var obj = {};
            obj[index] = {};
            for (var fx in this.effects)
                obj[index][fx] = 0;
            this.set(obj);
//            this.display(index);
        }
    }

});


/**
 * Extend Form.Validator with a requireIfChecked validation check
 */
Form.Validator.add('requiredIfChecked', {
    errorMsg: function(element, props) {
        return '{title} must be set.'.substitute({
          title: element.getProperty('title')
        });
    },
    test: function(element, props) {
        // if required field is available and set
        if (document.id(props.requiredIfChecked).checked) {
            return (element.value > 0);
        }
        return true;
    }
});


//
// Modal Backdrop class
//
var ModalBackdrop = new Class({
    options: {
      onComplete: Class.empty,
      onResize: Class.empty
    },

    initialize: function(options) {
        this.setOptions(options);

        // Add backdrop element
        if (!$('modalBackdrop')) {
            new Element('div', {'id': 'modalBackdrop',
                                'styles': { 'height': window.getHeight()+'px',
                                            'opacity' : 0,
                                            'display': 'none' }}).injectInside(document.body);
        }

        // Display effects
        this.fx = {
            overlayShow: new Fx.Tween('modalBackdrop', { property: 'opacity', duration: 'short', onComplete: this.onShowComplete.bind(this)}),
            overlayHide: new Fx.Tween('modalBackdrop', { property: 'opacity', duration: 'short', onComplete: this.onHideComplete.bind(this)})
        };

        // Attach the onResize event
        window.addEvent('resize', this.onWindowResize.bind(this));
    },

    display: function() {
        // Returns the display style of the modalBackdrop
        return $('modalBackdrop').style.display;
    },

    show: function() {
        // Set its display style to be visible
        $('modalBackdrop').style.display = '';

        if (window.ie6)
            this.hideSelects();

        // Resize the backdrop
        this.onWindowResize();
        this.fx.overlayShow.start(0, 0.75);
    },

    hide: function() {
        this.fx.overlayHide.start(0.75, 0);
    },

    onShowComplete: function() {
        this.options.onComplete();
    },

    onHideComplete: function() {
        $('modalBackdrop').style.display = 'none';

        if (window.ie6)
            this.showSelects();

        this.options.onComplete();
    },

    onWindowResize: function() {
        // Set the backdrop height to the page height first, while we then
        // calculate how high the page is. This prevents the backdrop height
        // from being the defining factor in page height on pages with a
        // short amount of content...
        $('modalBackdrop').style.height = window.getHeight() + 'px';

        // Set the height of the backdrop as appropriate
        var backdropHeight = 0;
        if (window.getScrollHeight() > window.getHeight()) {
            // The viewport is larger than the page, so use the viewport to
            // determine the size of the dialog background
            backdropHeight = window.getScrollHeight();
        } else {
            // The page exceeds the visible area of the page, so make the modal
            // backdrop span through to the end of the page
            backdropHeight = window.getHeight();
        }
        $('modalBackdrop').style.height = backdropHeight + 'px';


        // Call the hook function for children using ModalBackdrop
        this.options.onResize();

        return true;
    },

    hideSelects: function() {
        var selects = $$('select');
        for (var i = 0; i < selects.length; i++) {
            var thisSelect = selects[i];
            if (thisSelect.clientWidth == 0 || thisSelect.clientHeight == 0 ||
                (thisSelect.nextSibling != null && thisSelect.nextSibling.className == 'selectReplacement')) {
                continue;
            }

            var newSpan = document.createElement('span');

            newSpan.style.height = (thisSelect.clientHeight - 5) + 'px';
            newSpan.style.width = (thisSelect.clientWidth - 6) + 'px';
            newSpan.className = 'selectReplacement';

            try {
                newSpan.innerHTML = thisSelect.options[thisSelect.selectedIndex].innerHTML;
            } catch (e) { }


            thisSelect.cachedDisplay = thisSelect.style.display;
            thisSelect.style.display = 'none';
            thisSelect.parentNode.insertBefore(newSpan, thisSelect.nextSibling);

            thisSelect = null;
        }

        selects = null;
    },

    showSelects: function() {
        var selects = $$('select');
        for (var i = 0; i < selects.length; i++) {
            var thisSelect = selects[i];
            if (thisSelect.clientWidth == 0 || thisSelect.clientHeight == 0 ||
                thisSelect.nextSibling == null || thisSelect.nextSibling.className != 'selectReplacement') {
                continue;
            }
            thisSelect.parentNode.removeChild(thisSelect.nextSibling);
            thisSelect.style.display = thisSelect.cachedDisplay;

            thisSelect = null;
        }

        selects = null;
    }

});
ModalBackdrop.implement(new Options);



var ModalPrint = new Class({
    options: {
        url             : '',
        method          : 'get',
        jasperLocation  : '/jasperreports/createReport',
        format          : 'pdf'
    },

    initialize: function(options) {
        this.setOptions(options);
        this.initialized = false;

        window.addEvent('domready', this.domInitialize.bind(this));
    },

    domInitialize: function() {
        if (!$('modalPrint')) {
            // Create the dialog HTML elements
            new Element('div', {'id': 'modalPrint', 'styles': { 'visibility': 'hidden' }}).injectInside(document.body);
            new Element('img', {
                'id'        : 'modalPrintClose',
                'class'     : 'modalPrintClose',
                'src'       : '/_common/images/icons/symbols/cancel16.png',
                'height'    : 16,
                'width'     : 16
              }).injectInside('modalPrint');
            new Element('div', {'id' : 'modalPrintHeader', 'class' : 'modalPrintHeader' }).injectInside('modalPrint');
            new Element('div', {'id' : 'modalPrintContent', 'class' : 'modalPrintContent' }).injectInside('modalPrint');
            new Element('img', {
                'id'        : 'modalPrintLoading',
                'class'     : 'modalPrintLoading',
                'src'       : '/_common/images/elements/loading-ball.gif',
                'height'    : 16,
                'width'     : 16
              }).injectInside('modalPrintContent');
            new Element('a', { 'id' : 'modalPrintShow', 'class' : 'modalPrintShow', 'target' : '_blank', 'styles' : { 'display' : 'none' } }).injectInside('modalPrintContent');
            new Element('img', {
                'src'       : '/_common/images/icons/symbols/search32.png',
                'height'    : 32,
                'width'     : 32
              }).injectInside('modalPrintShow');
            new Element('br').injectInside('modalPrintShow');
            $('modalPrintShow').appendText('Show in Browser');

            new Element('div', { 'id' : 'modalPrintLinks', 'class' : 'modalPrintLinks' }).injectInside('modalPrintContent');

            new Element('a', { 'id' : 'modalPrintSave', 'class' : 'modalPrintSave', 'styles' : { 'display' : 'none' } }).injectInside('modalPrintLinks');
            new Element('img', {
                'src'       : '/_common/images/icons/symbols/down32.png',
                'height'    : 32,
                'width'     : 32
              }).injectInside('modalPrintSave');
            new Element('br').injectInside('modalPrintSave');
            $('modalPrintSave').appendText('Save to Desktop');

            new Element('div', {
                'id'        : 'modalPrintRegenerate',
                'class'     : 'modalPrintRegenerate',
                'styles'    : { 'display' : 'none' }
              }).injectInside('modalPrintContent');
            new Element('img', {
                'src'       : '/_common/images/icons/symbols/refresh16.png',
                'height'    : 16,
                'width'     : 16
              }).injectInside('modalPrintRegenerate');

           $('modalPrintRegenerate').appendText(' Reload Document');

            new Element('div', {
              'id': 'modalPrintStatusText',
              'class': 'modalPrintStatusText'
            }).injectInside('modalPrintLinks');

            new Element('div', {
              'id': 'modalPrintStatusErrorText',
              'class': 'modalPrintStatusErrorText'
            }).injectAfter('modalPrintStatusText');
        }

        // Per-dialog initialisation
        var nextEffect = this.nextEffect.bind(this);
        var windowResize = this.onWindowResize.bind(this);
        this.backdrop = new ModalBackdrop({ onComplete: nextEffect, onResize: windowResize });

        // Initialised
        this.initialized = true;
    },

    setFormat: function(format) {
        // Set the format type of this report
        this.format = format == 'zip' ? 'zip' : 'pdf';
    },

    show: function(args) {
        // Trap attempts to show the dialog before it is initialised
        if (!this.initialized)
            return;

        // Initialise the dialog state to clean up from previous displays
        this.step = 1;

        // Display the modal backdrop
        if (this.backdrop.display() == 'none')
            this.backdrop.show();
        else
            this.step = 2;
        // Set the default layout
        $('modalPrint').removeClass('modalPrintError');

        $('modalPrintClose').removeEvents('click');
        $('modalPrintHeader').setText('Creating Report');
        $('modalPrintLoading').show();
        $('modalPrintShow').hide();
        $('modalPrintShow').href = '';
        $('modalPrintSave').hide();
        $('modalPrintSave').href = '';
        $('modalPrintStatusErrorText').hide();
        $('modalPrintContent').setStyle('height', '');
        $('modalPrintRegenerate').hide();
        $('modalPrintRegenerate').removeEvents('click');
        $('modalPrintStatusText').setText('Please wait while the report is created');

        // Get the reporting key
        this.ajax = new Request({
            url         : this.options.url,
            method      : this.options.method,
            headers     : { 'X-SENTRAL-Relative-Root': rel_root },
            data        : args,
            onSuccess   : this.loadPDF.bind(this),
            onFailure   : this.error
          }).send();

        $('modalPrintRegenerate').addEvent('click', function() { this.show(args); return false; }.bind(this) );

        // Set position of dialog frame
        $('modalPrint').style.top = Math.max((window.getHeight() - $('modalPrint').offsetWidth) / 2, 0) + 'px';
        $('modalPrint').style.left = Math.max((window.getWidth() -  $('modalPrint').offsetHeight) / 2, 0) + 'px';

        // Add events
        $('modalPrintClose').addEvent('click', this.close.bind(this));

    },

    loadPDF: function(response) {
        // Check that the reponse is correct
        if (response.length == 20) {
            // We have a report key
            $('modalPrintHeader').setText('Loading Report');

            this.key = response;

            // Create the pdf
            this.ajax = new Request({
                url         : this.options.jasperLocation,
                method      : 'get',
                data        : 'action=db&key=' + this.key,
                onSuccess   : this.showPDF.bind(this),
                onFailure   : this.error
            }).send();

        } else {
            this.error(response);
        }
    },

    showPDF: function(response) {
        // Check that the response is correct
        if (response == 'OK') {
            // The report was created
            $('modalPrintHeader').setText('Report Created');
            $('modalPrintLoading').hide();

            $('modalPrintStatusText').setText('');

            $('modalPrintShow').show();
            $('modalPrintShow').href = this.options.jasperLocation + '?format=' + this.format + '&key=' + this.key;

            $('modalPrintSave').show();
            $('modalPrintSave').href = this.options.jasperLocation + '?action=save&format=' + this.format + '&key=' + this.key;

            $('modalPrintRegenerate').show();

            if (this.format == 'zip')
                location.assign(this.options.jasperLocation + '?format=' + this.format + '&key=' + this.key);
            else
                window.open(this.options.jasperLocation + '?format=' + this.format + '&key=' + this.key);
        } else {
            this.error(response);
        }
    },

    error: function(error) {
        // An error occured when creating this report
        $('modalPrint').addClass('modalPrintError');
        $('modalPrintHeader').setText('Error');
        $('modalPrintLoading').hide();

        if (typeof(error) == 'object') {
            // The tomcat service is not responsive, display the status code
            $('modalPrintStatusText').setText('Document Generation Service unresponsive. Returned status: ' + error.status);
        } else {
            if (error.substr(0, 19) == 'MODAL_PRINT_ERROR: ') {
                $('modalPrintStatusText').setText(error.substr(18));
            } else {
                $('modalPrintStatusText').set('text',
                  'An error has occured generating this document. ' +
                  'If this error persists, contact the Sentral Heldpesk ' +
                  'for assistance.'
                );
                $('modalPrintStatusErrorText').set('text', error).show();
                $('modalPrintContent').setStyle('height', 'auto');
            }

            $('modalPrintRegenerate').show();
        }

    },

    onWindowResize: function() {
        if (this.options.visible) {
            // Determine new horizontal and vertical location of dialog
            var dialogLeft = (window.getWidth() - $('modalPrint').offsetWidth) / 2;
            var dialogTop = (window.getHeight() - $('modalPrint').offsetHeight) / 2;
            dialogLeft = Math.max(dialogLeft, 0);
            dialogTop = Math.max(dialogTop, 0);

            // IE6 doesn't support position: fixed, so we need to allow
            // for the scroll height in calculating the new top position
            if (window.ie6)
                dialogTop += window.getScrollTop();

            // Move the dialog
            $('modalPrint').style.left = dialogLeft + 'px';
            $('modalPrint').style.top = dialogTop + 'px';
        }
    },

    // Event handlers
    nextEffect: function() {
        switch (this.step++) {
        /*
         * Opening animations
         */
            case 1:
                // Display the print dialog
                $('modalPrint').style.visibility = '';

                // In IE6, make sure we are in the right position
                if (window.ie6)
                    this.onWindowResize();

                break;

        /*
         * Closing animations
         */
            case 2:
                // Hide the print dialog
                $('modalPrint').style.visibility = 'hidden';


                this.backdrop.hide();
                break;
        }

    },

    close: function() {
        // Remove save/cancel button handlers
        $('modalPrintClose').removeEvents('click');

        this.key = null;

        // Try to cancel any outstanding ajax requests
        try {
            this.ajax.cancel();
        } catch (e) {

        }

        // Begin the close animations
        this.nextEffect();

    }
});
ModalPrint.implement(new Options);

var OverlayFix = new Class({
    initialize: function(el) {
        this.elementId = el;
        if (window.ie){
            $(this.elementId).addEvent('trash', this.destroy.bind(this));
            this.fix = new Element('iframe', {
                properties: {
                    frameborder: '0',
                    scrolling: 'no',
                    src: 'javascript:false;'
                },
                styles: {
                    position: 'absolute',
                    border: 'none',
                    display: 'none',
                    filter: 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
                }
            }).injectAfter($(this.elementId));
        }
    },

    show: function() {
        if (this.fix) this.fix.setStyles($extend(
            $(this.elementId).getCoordinates(), {
                display: '',
                zIndex: ($(this.elementId).getStyle('zIndex') || 1) - 1
            }));
        return this;
    },

    hide: function() {
        if (this.fix) this.fix.setStyle('display', 'none');
        return this;
    },

    destroy: function() {
        this.fix.dispose();
    }

});


//
// Sentral classes
//
var Sentral = new Class({
    Implements: [Options]
});

Sentral.Tips = new Class({
    Extends: Sentral,

    options: {
        classname: 'tips'
    },

    initialize: function(options) {
        this.setOptions(options);
        window.addEvent('domready', this.build.bind(this));
    },
    prepare: function() {
        if (this.options.classname) {
            $$('.' + this.options.classname).each(function(element, index) {
                if (element.get('title')) {
                    var content = element.get('title').split('::');
                    if (typeof content[1] != 'undefined') {
                        // The tip has a title and a text section
                        element.store('tip:title', content[0]);
                        element.store('tip:text', content[1]);
                    } else {
                        // No title, just display the text provided
                        element.store('tip:title', '');
                        element.store('tip:text', content[0]);
                    }
                }
            });
        }
    },
    build: function() {
        // Prepare the title & text
        this.prepare();

        // build tips
        this.base_tips = new Tips('.' + this.options.classname, {
            className: 'tooltip'
        });

        this.base_tips.addEvents({
            'show': function(tip) {
                tip.fade('in');
            },
            'hide': function(tip) {
                tip.fade('out');
            }
        });
    },
    attach: function(elements) {
        elements.each(function(element, index) {
            if (element.get('title')) {
                var content = element.get('title').split('::');
                element.store('tip:title', content[0]);
                element.store('tip:text', content[1]);
            }
        });
        this.base_tips.attach(elements);
    },
    hideAll: function() {
        this.base_tips.hide();
    }
});


//
// Sentral.Utils.CheckboxToggle
//   Manages mass select/unselect/toggle functionality for groups of checkboxes
//
Sentral.Utils = {};
Sentral.Utils.CheckboxToggle = new Class({
    Implements: [Options],

    options: {
        toggle: null
    },

    // The list of elements to toggle on/off
    elements: null,

    initialize: function(elements, options) {
        this.setOptions(options);
        this.elements = elements;
        if ($defined(this.options.toggle)) {
            window.addEvent('domready', function() {
                if ($(this.options.toggle).getProperty('type') == 'checkbox') {
                    $(this.options.toggle).addEvent('click', function() {
                        this.select($(this.options.toggle).get('checked'));
                    }.bind(this));
                } else {
                    $(this.options.toggle).addEvent('click', function() {
                        this.selectButton();
                    }.bind(this));
                }
            }.bind(this));
        };
    },

    select: function(state) {
        $$(this.elements).each(function(el) {
            if (el != $(this.options.toggle)) {
                el.set('checked', state);
                el.fireEvent('click', new Event.Mock(el));
            }
        }.bind(this));
    },

    selectButton: function() {
        // First get the total checked and total unchecked
        // to decide which behaviour to use
        var checked = 0;
        var unchecked = 0;
        $$(this.elements).each(function(el) {
            if (el.checked)
                checked++;
            else
                unchecked++;
        });

        var behaviour = (checked <= unchecked);
        $$(this.elements).each(function(el) {
            el.checked = behaviour;
        });
    },

    selectAll: function() {
        this.select(true);
    },

    deselectAll: function() {
        this.select(false);
    }
});

Sentral.Utils.TableHighlight = new Class({
    Implements: [Options],

    options: {
        toggles: null
    },

    table: null,

    // The table element to apply to
    initialize: function(table, options) {
        this.setOptions(options);
        this.table = table;

        // Attach to the row select checkboxes when the DOM is ready
        if ($defined(this.options.toggles)) {
            window.addEvent('domready', function() { this.attach(this.options.toggles) }.bind(this));
        }
    },

    toggleHighlight: function(el) {
        if (el.get('checked'))
            el.getParent('tr').addClass('highlight');
        else
            el.getParent('tr').removeClass('highlight');
    },

    attach: function(elements) {
        $(this.table).getElements(elements).each(function(el) {
            el.addEvent('click', this.toggleHighlight.bind(this, el));
        }.bind(this));
    }
});

Sentral.DropDown = new Class({
    Implements: [Options],

    Binds: [ 'showHandler', 'hideHandler', 'showDropDown', 'hideDropDown' ],

    options: {
      element: null,
      trigger: null
    },

    initialize: function(options) {
        this.setOptions(options);

        // Attach to the trigger
        if (this.options.trigger) {
            window.addEvent('domready', function() {
                document.id(this.options.trigger).addEvent('click', this.showHandler.bind(this));
            }.bind(this));
        }

        this.boundHideHandler = this.hideHandler.bind(this);

    },

    showHandler: function(e) {
        e.stop();
        this.showDropDown();
    },

    hideHandler: function(e) {
        e.stop();
        this.hideDropDown();
    },

    showDropDown: function() {
        // Display the drop down
        document.id(this.options.element).fade('in');

        // show the menu
        document.id(document.body).addEvent('click', this.boundHideHandler);

        this.shown = true;
    },


    hideDropDown: function() {
        if (this.shown) {
            // Hide the element
            document.id(this.options.element).fade('out');

            // Remove the document body onclick event
            document.id(document.body).removeEvent('click', this.boundHideHandler);

            this.shown = false;
        }
    }
});

/* AJAX student search */
Sentral.QuickSearch = new Class({
    Implements: [Options],
    options: {
        title           : null,
        search_value    : 'Search...',
        width           : '200',
        url             : '/ajax/searchStudent',
        auto_select     : false,
        delay           : 500,
        onSelectResult  : $empty,
        onSearchSuccess : $empty,
        onClose         : $empty,
        positionResults : true

    },

    // Construct takes an element and the Class options
    initialize: function(element, options) {
        this.setOptions(options);

        // Create the search elements
        this.elements = new Array;

        this.elements.container = new Element('div', {
                'class'     : 'sentral_quicksearch',
                'events'    : {
                    'mousemove' : this.resetNavigation.bind(this)
                }
            }).inject(element);

        if (this.options.title !== null) {
            // Insert a title if needed
            var title = new Element('h1').inject(this.elements.container);
            title.appendText(this.options.title)
        }

        // Create the input element
        this.elements.input = new Element('input', {
                'type'          : 'text',
                'class'         : 'input',
                'autocomplete'  : 'off',
                'name'          : this.options.field_name,
                'placeholder'   : this.options.search_value,
                'styles'    : {
                    'width' : this.options.width + 'px'
                },
                'events'        : {
                    'focus'     : this.focusSearch.bind(this),
                    'mouseup'   : this.mouseUpSearch.bind(this),
                    'click'     : function(e) { e.stopPropagation(); },
                    'keydown'   : this.navigateResults.bind(this),
                    'keyup'     : this.startSearch.bind(this)
                }
            }).inject(this.elements.container);

        // Attache SFx.Placeholder
        new SFx.Placeholder({ elements: this.elements.input });

        // Create a container for the search results
        this.elements.results_container = new Element('div', {
                'class' : 'results_container',
                'styles'        : {
                    'width' : this.options.width + 'px' }
            }).inject(this.elements.container);

        this.elements.results_container.hide();

        // Create a cancel button
        this.elements.cancel = new Element('img', {
                'class'     : 'cancel',
                'height'    : 16,
                'width'     : 16,
                'src'       : '/_common/images/icons/symbols/delete16.png',
                'events'    : {
                    'click' : this.stopSearch.bind(this)
                }
            }).inject(this.elements.results_container);

        // Create a search results container
        this.elements.results = new Element('div', {
                'class' : 'results'
            }).inject(this.elements.results_container);

    },

    focus:  function() {
        // Focus on the search box
        try {
            this.elements.input.focus();
        } catch (e) { }
    },

    focusSearch: function() {
        // What happens when the search box is focused
        this.elements.input.select();
    },

    mouseUpSearch: function(e) {
        // Prevent Safari/Chrome from unselecting the input
        e.preventDefault();
    },

    navigateResults: function(e) {
        // Handles the keyboard nagivation between results
        switch((window.event) ? window.event.keyCode : e.event.keyCode) {
            case 16: case 17: case 18: case 19: case 37: case 39:
                //Shift, Ctrl, Alt, Pause, Arrow Left, Arrow Right

                // Do nothing
                break;

            case 38:
                //Arrow Up

                // Select the previous element on the list
                var previous_element = this.elements.results_container.getElements('tr.result').getLast();
                var selected_element = this.elements.results_container.getElement('tr.result.selected');

                if (selected_element != null) {
                    // We already have a selected element
                    selected_element.removeClass('selected');

                    if (selected_element.getPrevious('tr.result') != null)
                        previous_element = selected_element.getPrevious('tr.result');
                };

                if (previous_element != null)
                    previous_element.addClass('selected');

                break;

            case 40:
                // Arrow Down

                // Select the next element on the list
                var next_element = this.elements.results_container.getElement('tr.result');
                var selected_element = this.elements.results_container.getElement('tr.result.selected');

                if (selected_element != null) {
                    // We already have a selected element
                    selected_element.removeClass('selected');

                    if (selected_element.getNext('tr.result') != null)
                        next_element = selected_element.getNext('tr.result');
                };

                if (next_element != null)
                    next_element.addClass('selected');

                break;

            case 13:
                // Enter

                // Simulate a mouse click on the selected element
                var selected_element = this.elements.results_container.getElement('tr.result.selected');

                if (selected_element != null)
                    selected_element.fireEvent('click');

                return false;
                break;
        }

    },

    resetNavigation: function(e) {
        // If the mouse has moved, remove any selected element
        var selected_element = this.elements.results_container.getElement('tr.result.selected');

        if (selected_element != null)
            selected_element.removeClass('selected');
    },

    startSearch: function(e) {
        // Add an event to the text input to control the key strokes
        switch((window.event) ? window.event.keyCode : e.event.keyCode) {
            case 13:
                // If auto select on enter is enabled, do a search with auto selection on
                if (this.options.auto_select == true && this.elements.results_container.getElement('tr.result.selected') == null) {
                    this.auto_select = true;
                    this.elements.input.select();
                    $clear(this.searchTimer);
                    this.searchTimer = this.performSearch.delay(500, this);
                }
                break;

            case 16: case 17: case 18: case 19: case 37: case 38: case 39: case 40:
                // Don't search on these keys
                break;

            default:
                // Otherwise engage the search timer
                this.auto_select = false;
                $clear(this.searchTimer);
                this.searchTimer = this.performSearch.delay(500, this);
            }
    },

    performSearch: function() {
        if (this.elements.input.value && this.elements.input.value != this.options.search_value) {

            if (this.request)
                this.request.cancel();

            this.elements.results.empty();
            this.elements.results_container.show();
            if (this.options.positionResults) {
                var x_input = this.elements.input.getPosition().x;
                var y_input = this.elements.input.getPosition().y + this.elements.input.getSize().y;
                this.elements.results_container.setPosition({
                    x : x_input,
                    y : y_input
                  });
            }

            this.request = new Request.JSON({
                url             : this.options.url,
                data            : 'query=' + this.elements.input.value,
                method          : 'get',
                onSuccess       : this.displayResults.bind(this)
            }).send();
        }
    },

    stopSearch: function() {
        if (this.request)
            this.request.cancel();

        // Run a custom close function
        this.options.onClose();

        this.elements.results_container.hide();

        try {
          // Try to focus this element, but ignore if it doesn't work
          this.elements.input.focus();
        } catch (e) { }
    },

    resetSearch: function() {
        // Stops the search and sets the input back to its default
        this.stopSearch();
        this.elements.input.value = ''; //this.options.search_value;
    },

    displayResults: function(response) {
        // Run a custom search success function
        this.options.onSearchSuccess();

        this.elements.results.empty();

        // Build the results display
        var table = new Element('table').inject(this.elements.results);
        var table_body = new Element('tbody').inject(table);

        response.results.each(function(result) {
            // Display each result
            var table_row = new Element('tr', {
                    'class'     : 'result' + (result['class'] != undefined ? ' ' + result['class'] : ''),
                    'events'    : {
                        'click' : this.options.onSelectResult.pass([result.id, result], this)
                    }
                }).inject(table_body);

            if (result.icon) {
                // If the results include an icon, place it here
                var table_data = new Element('td', {
                        'class' : 'icon'
                    }).inject(table_row)

                new Element('img', {
                        'src'       : result.icon,
                        'height'    : response.icon_size,
                        'width'     : response.icon_size
                    }).inject(table_data)
            }

            // Title data
            var table_data = new Element('td', {
                    'class'     : 'title',
                    'colspan'   : (result.icon ? '1' : '2')
                }).inject(table_row);
            table_data.appendText(result.title);

            // Description data
            var table_data = new Element('td', {
                    'class' : 'description'
                }).inject(table_row);

            if (result.description)
                table_data.appendText(result.description);
        }.bind(this));

        if (response.more_results) {
            // This search returns more rows
            var table_row = new Element('tr', {
                    'class'     : 'more_results'
                }).inject(table_body);

            var table_data = new Element('td', {
                    'colspan'   : 3
                }).inject(table_row);
            table_data.appendText('More results found, please refine the search');
        }

        if (response.results.length == 0) {
            // No results found
            var table_row = new Element('tr', {
                    'class' : 'no_result'
                }).inject(table_body);

            var table_data = new Element('td').injectInside(table_row);
            table_data.appendText('No results found.');
        }

        if (this.auto_select && response.results.length == 1) {
            // Auto select the only result
            this.elements.results_container.hide();
            this.options.select_result.attempt(response.results[0].id);
        }
    }

});

Sentral.Navigation = new Class({
    Implements: [ Options ],

    // all Fx.Accordion options can be passed in
    // but be aware that 'show' will override
    // 'display' and the menu will not work as intended
    options: {
        display: 0
    },

    initialize: function(container, toggleClass, elementClass, options){
        this.setOptions(options);

        // menu elements
        this.container = container;
        this.tClass = toggleClass;
        this.eClass = elementClass;

        // array to hold Accordion objects
        this.menu = $H();

        // starting menu selector
        this.selector = '#' + this.container + ' > ul > li ';

        // the current level of menu being created
        this.level = 1;

        // sets information in the div element
        // for use in determining which should
        // be open by default
        this.setPositionData();

        // the position to expand for each level of accordion
        this.active_positions = [];

        // build position array for use in with Fx.Accordion's 'show' option
        this.setActivePositions();

        // finally build the menu accordions
        this.makeAccordion();

        this.setDynamicMenus();
    },

    makeAccordion: function(){
        // set the appropriate menu as default at each accordion level
        // if no active menu then close every menu below level 1 by default
        var pos = (this.active_positions[this.level] != null) ?  this.active_positions[this.level].toInt() : -1;
        // var dur = this.level > 1 ? 500 : 0;
        var dur = 0;

        this.setOptions({
            display: pos,
            initialDisplayFx: true,
            alwaysHide: true,
            duration: dur
        });
        // this.options.display = this.active_positions[this.level].toInt();
        // this.options.show = this.active_positions[this.level].toInt();
        this.menu[this.level] = new Accordion(
            $$(this.selector+' > '+this.tClass),
            $$(this.selector+' > '+this.eClass),
            this.options
        ).addEvents({
            // The onActive and onComplete events added to the stack here to
            // attempt to address some of the css issues.
            'onActive': function(toggle){
                if(toggle.getParent().getStyle('height') != 0)
                    toggle.getParent().setStyle('height', '');
            },
            'onComplete': function(a){
                this.options.duration = 500;
                if (a != null) {
                    var height = 0;
                    a.getParent().getChildren().each(function(e){
                        height = height + e.offsetHeight;
                    });
                    if(height != a.getParent().offsetHeight && a.getParent().offsetHeight != 0)
                        a.getParent().setStyle('height','');
                }
            }
        });

        // set selector to be one level deeper
        this.selector += ' > ' + this.eClass + ' > ul > li ';
        // if this next level exists recursively create menus for this level
        if($$(this.selector+' > '+this.eClass)[0] != null) {
            this.level++;
            this.makeAccordion();
        }
    },

    setActivePositions: function() {
        // the first active menu item
        var active = document.id(this.container).getElement('.active');

        if (active != null) {
            var active_level = active.getProperty('data-level');

            for (var i = active_level; i > 0; i--) {
                this.active_positions[i] = active.getParent('div[data-level="'+i+'"]').getProperty('data-position');
            }

            // add an icon if the active item is at depth 0
            if (active_level == 0) {
                // new Element('img', {src: '/_common/images/nav/active-depth0.png', style: 'margin-left: 4px;'}).inject(active);
            }

            // make sure active section is visible
            active.getParent('li.depth0').show();
        }
    },

    setPositionData: function() {
        // get deepest level
        for (i = 1; i < 10; i++) {
            document.id(this.container).getElements('div[data-level="'+i+'"]').each(function(div, index) {
                div.set('data-position', index);
            });
        }
    },

    setDynamicMenus: function() {
        // find all dynamic menu items
        $$('a[data-type="dynamic"]').each(function(el) {
            el.addEvent('click', function(e) {
                e.stop();
                var link = e.target.getProperty('data-link');
                var group = e.target.getProperty('data-group');

                $$('li[data-group="'+group+'"]').each(function(section) {
                    this.menu[1].removeSection(section.getChildren('a'));
                    section.hide();
                }.bind(this));

                var section = $$('li[data-group='+group+'][data-link='+link+']');
                var toggler = section.getChildren('a');
                var element = section.getChildren('div');
                section.show();
                this.menu[1].display(element[0].getProperty('data-position'));
            }.bind(this));
        }.bind(this));
    }
});


/*
 *
 * XXX BEGIN Legacy Quick Search
 * Required by Welfare that has a particularly complex implementation
 * of Quick Search, which needs to be rolled out ASAP
 *
 */

var QuickSearchId = 1;
var QuickSearchElementId = {};
Sentral.LegacyQuickSearch = new Class({
    Implements: [Options],
    options: {
        field_name: 'search',
        search_value: 'Search...',
        width: '200',
        url: '/ajax/searchStudent',
        fn: 'selectStudent',
        result_class: 'floatSearch',
        callback: '',
/*        onfocus: '',
        onsearch: '',
        oncancel: '',*/
        data: {}
    },

    // Construct takes an element and the Class options
    initialize: function(element, options) {
        this.setOptions(options);
        this.container = element;
        this.id = QuickSearchId++;

        // Link id to container
        QuickSearchElementId[this.container] = this.id;

        // Construct the DOM inside the container
        window.addEvent('domready', function() {
            // Create an input text
            var input_div = new Element('div').inject(this.container);
            var input = new Element('input', {'type' : 'text',
                                              'id' : this.options.field_name + this.id,
                                              'styles' : {'width' : this.options.width + 'px'},
                                              'autocomplete' : 'off',
                                              'name' : this.options.field_name,
                                              'alt': this.options.search_value,
                                              'value': this.options.search_value}).inject(input_div);

            // Add the onfocus events
            input.addEvent('focus', function() { input.select(); }.pass(input.event));
            if ($type(this.options.onfocus) == 'function')
                input.addEvent('focus', function() { return this.options.onfocus(); }.bind(this));

            // Stop any click events to the input from bubbling up
            input.addEvent('click', function(ev) { ev.stopPropagation(); }.pass(input.event));

            // Create a search result & cancel button container
            this.search_result_container = new Element('div', {
                'id': 'search_result_container' + this.id,
                'class': this.options.result_class,
                'styles': { 'position': 'absolute', 'display': 'none'}
            }).inject(input_div, 'after');
            this.search_result_container.hide();

            // Create an element for cancelling the search
            this.search_cancel = new Element('div', {'class' : 'floatSearchCancel'}).inject(this.search_result_container);
            new Element('img', {'height' : 16,
                                'width' : 16,
                                'src' : '/_common/images/icons/symbols/delete16.png'}).inject(this.search_cancel);

            this.search_cancel.addEvent('click', this.stopSearch.bind(this));
            this.search_cancel.addEvent('click', function(ev) { ev.stopPropagation(); }.pass(this.search_cancel.event));

            // Create a search result container
            this.search_result = new Element('div', {'id' : 'search_result' + this.id}).inject(this.search_result_container);

            // add an event to the text input
            input.addEvent('keydown', function(e) {
                switch((window.event) ? window.event.keyCode : e.event.keyCode){
                case 16: case 17: case 18: case 19: case 37: case 39: break;//shift, ctrl, alt, pause, arrow left, arrow right
                case 38://arrow up
                    var elms = this.search_result_container.getElementsByTagName("*");
                    var found=-1, first=null, last=null;
                    for(var i = elms.length-1; 0 <= i; --i) {
                       if(elms[i].className == 'floatSearchRow'){
                            if(last == null) last = elms[i];
                            first=elms[i];
                            if(found == 1){ elms[i].className = 'floatSearchRow floatSearchSelected'; found = 0;}
                        }
                        else if(elms[i].className == 'floatSearchRow floatSearchSelected'){
                            if(last == null) last = elms[i];
                            first=elms[i];
                            elms[i].className = 'floatSearchRow'; found = 1;
                        }
                    }
                    if(found==1 && first!=null) first.className = 'floatSearchRow floatSearchSelected';
                    if(found==-1 && last!=null) last.className = 'floatSearchRow floatSearchSelected';
                    break;
                case 40://arrow down
                    var elms = this.search_result_container.getElementsByTagName("*");
                    var found=-1, first=null, last=null;
                    for(var i = 0; i < elms.length; ++i) {
                       if(elms[i].className == 'floatSearchRow'){
                            if(first == null) first = elms[i];
                            last=elms[i];
                            if(found == 1){ elms[i].className = 'floatSearchRow floatSearchSelected'; found = 0;}
                        }
                        else if(elms[i].className == 'floatSearchRow floatSearchSelected'){
                            if(first == null) first = elms[i];
                            last=elms[i];
                            elms[i].className = 'floatSearchRow'; found = 1;
                        }
                    }
                    if(found==1 && last!=null) last.className = 'floatSearchRow floatSearchSelected';
                    if(found==-1 && first!=null) first.className = 'floatSearchRow floatSearchSelected';
                    break;
                case 13://enter
                    var elms = this.search_result_container.getElementsByTagName("*");
                    for(var i = 0; i < elms.length; ++i) {
                       if(elms[i].className == 'floatSearchRow floatSearchSelected'){
                           if(elms[i].onclick != null) elms[i].onclick.apply(this,new Array(e));
                           elms[i].className = 'floatSearchRow';
                           return false;
                        }
                    }
                    return false;
                    break;
                }
            }.bind(this));

            input.addEvent('keyup', function(e) {
                switch((window.event) ? window.event.keyCode : e.event.keyCode){
                case 13: case 16: case 17: case 18: case 19: case 37: case 38: case 39: case 40: break;
                default: this.searchTimeout();
                }
            }.bind(this));

            input.addEvent('mousemove', function(e) {
                var elms = this.search_result_container.getElementsByTagName("*");
                for(var i = 0; i < elms.length; ++i) {
                    if(elms[i].className == 'floatSearchRow floatSearchSelected')
                        elms[i].className = 'floatSearchRow';
                }
            }.bind(this));
        }.bind(this));

    },

    searchTimeout: function() {
        // If the search timer exists, clear it and then set it again
        try { clearTimeout(this.quick_search_timer); } catch (err) { };
            this.quick_search_timer = setTimeout(function() { this.search(); }.bind(this), 500);
    },

    search: function() {
        var value = $(this.options.field_name + this.id).value;
        if (value != '') {
            new Request.HTML({
              url:      this.options.url,
              method:   'get',
              data:     Object.toQueryString(Object.merge(this.options.data, {
                'query':    $(this.options.field_name + this.id).value,
                'fn':       this.options.fn,
                'callback': this.options.callback })),
              update:   this.search_result,
              evalScripts: true,
              onComplete: function() {
                if ($type(this.options.onsearch) == 'function')
                        this.options.onsearch();
                this.search_result_container.show();
              }.bind(this)
            }).send();
        }
    },

    stopSearch: function() {
          if ($type(this.options.onsearch) == 'function')
                  this.options.oncancel();
      this.search_result_container.hide();
    }
});

/*
 * END Legacy Quick Search
 */


Sentral.AutoGrow = new Class({
    Implements: [Options],

    options: {
        min_height: 15,
        max_height: 300,//-1 for no maximum hight
        extra_padding: 15,
        timer_timeout: 10,
        class_name: ''
    },

    textarea: null,
    text_size: null,
    timer: null,

    initialize: function(textarea, options) {
        this.setOptions(options);

        // Attach the AutoGrower to the elements onkeyup event
        if ($defined($(textarea))) {
            this.textarea = $(textarea);
            this.textarea.addEvent('keydown', this.pollTimer.bind(this));
            this.growElement(this);
        } else {
            window.addEvent('domready', function() {
              this.textarea.addEvent('keydown', this.pollTimer.bind(this));
              this.growElement(this);
            }.bind(this));
        }

        // Attach this to the window onresize event
        // IF statement prevents IE6 from carking it
        if (!Browser.Engine.trident || Browser.Engine.version > 4)
            window.addEvent('resize', this.growElement.bind(this));
    },

    pollTimer: function() {
        // Starts the timer to grow the element, this allows for a lot of typing without constantly growing
        // the textarea
        try {
            clearTimeout(this.timer);
        } catch(e) { }

        this.timer = setTimeout(this.growElement.bind(this), this.options.timer_timeout);
    },

    growElement: function() {
        // Grows the element to the size of the text inside it

        // Create a div with the text in it if one is not already present
        if (!this.text_size) {
            this.text_size = new Element('div', {
              'text'    : this.textarea.value + ' ',
              'class'   : this.options.class_name,
              'styles'  : {
                  'position'    : 'absolute',
                  'left'        : '10px',
                  'top'         : '10px',
                  'background-color' : 'white',
                  'width'   : this.textarea.getScrollSize().x + 'px',
                  'visibility'  : 'hidden',
                  'padding' : '0 3px',
                  'font-size' : this.textarea.style.fontSize,
                  'font-weight' : this.textarea.style.fontWeight,
                  'font-family' : this.textarea.style.fontFamily
                }
            }).inject($(this.textarea), 'after');
            if (Browser.Engine.trident && Browser.Engine.version <= 5)
                this.text_size.setStyle('word-wrap', 'break-word');
            else
                this.text_size.setStyle('white-space', 'pre-wrap');

            this.last_height = 0;
        } else {
            var textwidth = parseInt(this.textarea.getScrollSize().x)-6;
            if (textwidth < 0)
                textwidth = 0;
            this.text_size.setStyle('width', textwidth + 'px');
            this.text_size.set('text', this.textarea.value + ' ');
        }

        // Get the height of this div and make sure it is within the minimum and maximum dimensions
        var text_height = this.text_size.getSize().y + this.options.extra_padding + 10;
        if (this.options.max_height != -1)
            text_height = text_height.limit(this.options.min_height, this.options.max_height);
        else if (text_height < this.options.min_height)
            text_height = this.options.min_height;

        // Set the size of the textarea to be the text height
        if (this.last_height != text_height || text_height != this.textarea.getScrollSize().y) {
            this.textarea.setStyle('height', text_height + 'px');
            this.last_height = text_height;
        }

    }

});

/* Sentral Options */
Sentral.Options = new Class();

Sentral.Options.add = function(element, elementClass, quantity) {
  // Adds an element to the selected options if it is not already added
  var element = $(element);

  // The first option is never added
  if (element.selectedIndex == 0)
    return false;

  var container_element = element.getParent();

  var already_selected = false;

  // Find any currently selected items and if we already have this as a selected element then we don't
  // need to continue
  container_element.getElements('span.sentral-options-selected').each(function(selected_element) {
    if (selected_element.getElement('input').value == element.value)
      already_selected = true;
    });

  if (!already_selected) {
    // This element is not in the list of selected elements so add it
    var new_selection = new Element('span', {
      'class' : 'sentral-options-selected'
    }).inject(container_element, 'bottom');

    container_element.appendText(' ');

    var new_selection_input = new Element('input', {
      'type'  : 'hidden',
      'value' : element.value,
      'name'  : element.options[0].value + '[]'
    }).inject(new_selection);

    if (elementClass != '')
        new_selection_input.addClass(elementClass);

    // Show quantity input if more than 1
    if (quantity > 1) {
      new_selection.set('style', 'padding: 5px; line-height: 26px;');

      var quantity_select = new Element('select', { 'name': 'quantity_' + element.options[0].value + '[]' }).inject(new_selection, 'bottom');
      for (x = 1; x <= quantity; x++)
        new Element('option', { 'value': x }).inject(quantity_select, 'bottom').set('text', x);
    }

    new_selection.appendText(' ' + element.options[element.selectedIndex].label);

    var new_selection_remove = new Element('a', {
      'events'  : {
        'click' : function() { Sentral.Options.remove(this); return false; }
      },
      'href'    : '#'
    }).inject(new_selection, 'bottom');

    new_selection_remove.set('text', 'x');
  }

  // Set the selection box back to the beggining
  element.selectedIndex = 0;

}

Sentral.Options.remove = function(element) {
  // Removes an element for the selected options
  var element = $(element);

  var container_element = element.getParent();

  container_element.dispose();
}

/* Sentral Checkboxes */
Sentral.Checkboxes = new Class();

Sentral.Checkboxes.toggle = function(element) {
    // Toggles the disabled status of a Sentral Checkbox
    var input_element = $(element).getElement('input');

    if (input_element.disabled) {
        // Enable this element
        input_element.disabled = false;
        $(element).removeClass('disabled');
    } else {
        // Disable this element
        input_element.disabled = true;
        $(element).addClass('disabled');
    }
}

Sentral.Checkboxes.enable = function(element) {
    // Enable this element
    var input_element = $(element).getElement('input');

    input_element.disabled = false;
    $(element).removeClass('disabled');
}

Sentral.Checkboxes.disable = function(element) {
    // Disable this element
    var input_element = $(element).getElement('input');

    input_element.disabled = true;
    $(element).addClass('disabled');
}

/* Sentral MultiSelect */
Sentral.MultiSelect = new Class();

Sentral.MultiSelect.click = function(element) {
  // A Multiselect Button was clicked
  var element = $(element);

  if (!element.hasClass('selected')) {
    // This element is not currently selected
    $(element).getElement('input').checked = true;
    $(element).getParent().getElements('label').each(function(item) {item.removeClass('selected');});
    $(element).addClass('selected');
  }

}


//
// Sentral Calendar Element
//

var CalendarId = $H();

Sentral.Calendar = new Class({
    Implements: [Options],

    options: {
      containerId: null,
      style: null,
      date: new Date().getTime()
    },

    initialize: function(name, parent, options) {
      this.name = name;
      this.parent = parent;
      this.setOptions(options);

      // If the name passed is an array, e.g. "var[]", set an incremental
      // integer for the id to keep it unique
      if (this.name.match(/\[\]/g) == '[]') {
        // Set an index for this name if it doesn't exist
        if (typeof CalendarId[this.name] === 'undefined')
            CalendarId[this.name] = 1;

        this.id = this.name.replace(/\[\]$/, CalendarId[this.name]);

        // Increment the id integer for the next calendar element
        CalendarId[this.name]++;
      } else
          this.id = this.name;

      this.add();
    },

    add: function() {
      var date = Date.parse(this.options.date);

      var containerDiv = new Element('div', { 'class': 'date_div',
                                     'id': this.options.containerId,
                                     'style': this.options.style
      }).inject(this.parent);

      var date_span = new Element('span', { 'id': this.id }).inject(containerDiv);
      date_span.setText(date.getDate() + '/' + (date.getMonth()+1) + '/' + date.getFullYear() + ' ');

      var img = new Element('img', { 'src': '/_common/images/icons/symbols/calendar16.png',
                                     'style': 'width: 16px; height: 16px;',
                                     'alt': 'Choose Date'
      }).inject(containerDiv);
      img.addEvent('click', function(el, id) { showCalendar(el, id); }.pass([img, this.id]));

      new Element('input', { 'type': 'hidden',
                             'id': this.id + '_input',
                             'name': this.name,
                             'value': date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate()
      }).inject(this.parent);
    }
});

var EditRows = $H();
Sentral.EditRow = new Class({
    Implements: [Options],

    options: {
        deleteButton: false,
        editButton: true,
        reloadOnSave: false,
        reloadOnDelete: false,
        removeCallback: false,
        url: document.location.href,
        inputWidth: '80%'
    },

    initialize: function(name, options) {
        if (EditRows.getLength() == 0)
            this.firstRow = true;

        EditRows[name] = this;

        this.setOptions(options);
        this.items = [];

        // Use current location as default URL if none is specified
        if (!this.options.url)
            this.options.url = window.location.href;

        this.request = new Request({
            'url': this.options.url,
            'method': 'post',
            onRequest: function() { this.saving = true; }.bind(this),
            onSuccess: function(response) {
                this.saving = false;

                if (response == 'OK') {
                    if ((this.options.reloadOnSave && !this.deleting) || (this.options.reloadOnDelete && this.deleting))
                        window.location.reload();

                    this.activeItem.saveOk();
                    this.activeItem.doActions();
                    this.activeItem = this.queuedItem;
                    this.queuedItem = null;
                } else {
                    this.activeItem.saveFail();
                    this.activeItem.doActions();
                }
            }.bind(this)
        });

        window.addEvent('domready', function(name) { this.domInitialize(name); }.bind(this).pass(name));
    },

    domInitialize: function(name) {
        // Create new object of EditRowItem and add it to the array
        var id = 1;
        $$(name).each(function(el) {
            var item = new Sentral.EditRowItem(id, el, this, { 'deleteButton': this.options.deleteButton,
                                                               'removeCallback': this.options.removeCallback,
                                                               'editButton': this.options.editButton,
                                                               'inputWidth': this.options.inputWidth
                                                             });
            this.items[id] = item;
            id++;
        }, this);

        // Create instance of Sentral Tips
        new Sentral.Tips();

        // Save active rows when user clicks away from element
        if (this.firstRow) {
            // Cycle through each row object and call the save method
            // to ensure that clicking away saves all edited rows
            document.addEvent('click', function() {
                EditRows.each(function(row) {
                    row.doActions();
                });
            });
        }
    },

    doActions: function() {
        if (this.activeItem)
            this.activeItem.doActions();
    },

    // this.queuedItem is used when AJAX is being fired.
    // Otherwise there is a race condition as to who finishes
    // first. If setActiveItem finishes first then the AJAX
    // will retoggle the new element, not the old one
    setActiveItem: function(item) {
        if (item.isActive())
            this.activeItem = null;
        else if (this.saving)
            this.queuedItem = item;
        else
            this.activeItem = item;
    },

    remove: function() {
        delete this.items[this.activeItem.id];
    }
});

Sentral.EditRowItem = new Class({
    Implements: [Options],

    options: {
        deleteButton: false,
        removeCallback: false,
        inputWidth: '80%',
        editButton: true
    },

    initialize: function(id, el, parent, options) {
        this.id = id;
        this.container = el;
        this.parent = parent;
        this.status = 'ok';
        this.setOptions(options);

        var view = el.getChildren('.view');
        this.view = view[0];

        var edit = el.getChildren('.edit');
        this.edit = edit[0];

        // Correct the size of any text input
        this.edit.getElements('input[type=text]').each(function(input) {
            input.setStyle('width', this.options.inputWidth);
        }.bind(this));

        if (this.options.deleteButton)
            this.deletebtn = new Element('img', {
                'src': '/_common/images/icons/symbols/delete12.png',
                'title': 'Delete',
                'style': 'width: 12px; height: 12px; float: right; padding: 3px;'
            }).inject(this.edit, 'top');

        // Setup tooltip
        this.container.addClass('tips');
        this.container.setProperty('title', '::Double click to edit');

        // Set indicator symbols on end of container
        this.okIcon = new Element('span', { 'style': 'float: right; opacity: 0; filter:alpha(opacity=0); display: none; padding: 3px;' }).inject(this.container, 'top');
        new Element('img', { 'src': '/_common/images/icons/symbols/tick12.png' }).inject(this.okIcon);
        this.okIcon.set('tween', { property: 'opacity', duration: 600 });

        this.failIcon = new Element('span', { 'style': 'float: right; opacity: 0; filter:alpha(opacity=0); display: none; padding: 3px;' }).inject(this.container, 'top');
        new Element('img', { 'src': '/_common/images/icons/symbols/warning12.png' }).inject(this.failIcon);
        this.failIcon.set('tween', { property: 'opacity', duration: 600 });

        if (!this.options.editButton) {
            this.editIcon = new Element('span', { 'style': 'float: right; opacity: 0.5; filter:alpha(opacity=50); padding: 3px;' })
        } else {
            this.editIcon = new Element('span', { 'style': 'float: right; opacity: 0.5; filter:alpha(opacity=50); padding: 3px;' }).inject(this.container, 'top');
            new Element('img', { 'src': '/_common/images/icons/symbols/edit12.png' }).inject(this.editIcon);
            this.editIcon.set('tween', { property: 'opacity', duration: 400 });
        }
        // Set effects for container
        this.container.addEvent('mouseover', function() { this.get('tween').start(0.5, 1); }.bind(this.editIcon));
        this.container.addEvent('mouseout', function() { this.get('tween').start(0.5); }.bind(this.editIcon));
        this.container.setStyle('cursor', 'pointer');

        // Hide edit container
        this.edit.setStyle('display', 'none');

        // Toggle View / Edit
        this.container.addEvent('dblclick', function() { this.doActions(); return false; }.bind(this));

        // Capture single click to stop unnecessary save() calls
        this.container.addEvent('click', function() {
            if (this.parent.activeItem && this.parent.activeItem != this)
                this.parent.activeItem.doActions();

            return false;
        }.bind(this));

        // Delete item
        if (this.options.deleteButton)
            this.deletebtn.addEvent('click', function() { this.remove(); }.bind(this));
    },

    doActions: function() {
        if (!this.isActive() && this.parent.activeItem)
            this.parent.activeItem.doActions();

        if (this.isActive() && this.getValue() != this.cacheValue)
            this.save();
        else
            this.toggle();
    },

    toggle: function() {
        this.view.toggle();
        this.edit.toggle();
        this.editIcon.toggle();

        // Toggle status icons
        if (this.status == 'fail') {
            this.failIcon.style.display = this.editIcon.style.display;
            this.okIcon.hide();
        } else {
            this.okIcon.style.display = this.editIcon.style.display;
            this.failIcon.hide();
        }

        // Save current values as query string for comparison on save()
        // This allows us to cater for any number of fields in the container
        this.cacheValue = this.getValue();

        // Toggle active item that is set in parent
        this.setActive();
    },

    save: function() {
        // Save edit string via AJAX
        this.parent.request.send(this.getValue());

        // Update cache value
        this.cacheValue = this.getValue();
    },

    saveOk: function() {
        this.status = 'ok';

        this.okIcon.show();
        this.okIcon.get('tween').start(0, 1).chain(
            function() { setTimeout(function() { this.start(1, 0); }.bind(this), 3000); },
            function() { this.set('display', 'none'); }
        );

        // Update View string
        var display = this.edit.getChildren('.display');
        if (display[0].get('tag') == 'select')
            var value = display[0].options[display[0].selectedIndex].get('text');
        else
            var value = display[0].get('value');
        this.view.set('text', value);

        // Run Callback
        if (typeof this.saveCallback == 'function')
            this.saveCallback(this.container);
        else if (this.saveCallback)
            eval('this.' + this.saveCallback + '()');
    },

    saveFail: function() {
        this.status = 'fail';

        this.failIcon.show();
        this.failIcon.get('tween').start(0, 1);
    },

    remove: function() {
        if (confirm('Are you sure you want to delete this item?')) {
            new Element('input', { 'type': 'hidden', 'name': 'deleteRow', 'value': 'true' }).inject(this.edit);

            if (this.options.removeCallback)
                this.saveCallback = this.options.removeCallback;
            else
                this.saveCallback = 'removeCallback';

            this.parent.deleting = true;
            this.save();
        }
    },

    removeCallback: function() {
        this.container.dispose();
        this.parent.dispose();
    },

    isActive: function()    { return (this.parent.activeItem && this.parent.activeItem.container == this.container); },
    setActive: function()   { this.parent.setActiveItem(this); },
    getValue: function()    { return this.edit.getValues().toQueryString(); }
});



/**
 * New Sentral Fx class library - SFx.*
 */



//
// SFx.Placeholder :
//
//    Takes the title= attribute of a text input and displays it in the text
//    box until the text box is focused. Automatically restores it if no text
//    is entered in to the text box.
//
var SFx = new Class({
    Implements: [Options]
});

// Override .get('value') and .set('value')
Element.Properties.value = {

    set: function(value) {
        if (this.retrieve('sfx:placeholder')) {
            if (value == '' || value == this.retrieve('sfx:placeholder:text')) {
                this.retrieve('sfx:placeholder').restorePlaceholder(this);
            } else {
                this.retrieve('sfx:placeholder').clearPlaceholder(this);
                this.value = value;
            }
        } else {
            this.value = value;
        }
    },

    get: function() {
        if (this.value && this.value == this.retrieve('sfx:placeholder:text'))
            return '';
        else
            return this.value;
    }

};

SFx.Placeholder = new Class({

    Implements: [Options],

    options: {
        className: 'sfx-placeholder-label',
        elements: 'input[type=text]',
        clearOnSubmit: true
    },

    initialize: function(options) {
        // Abort for browsers that support placeholder natively
        var supportsPlaceholder = ('placeholder' in document.createElement('input'));
        if (supportsPlaceholder)
            return;

        // Set the options
        this.setOptions(options);

        // Bound functions
        this.bound = {};
        this.bound.onFocus = this.onFocus.bind(this);
        this.bound.onBlur = this.onBlur.bind(this);

        // Check how to interpret the elements string
        var elements;
        switch (typeOf(this.options.elements)) {
            case 'string':
                elements = document.getElements(this.options.elements);
                break;

            case 'element':
                elements = [this.options.elements];
                break;

            default:
                elements = this.options.elements;
        }

        // Apply the check to each selector
        elements.each(function(el) {
            if (!el.retrieve('sfx:placeholder'))
                this.attach(el);
        }.bind(this));
    },

    attach: function(el) {
        // If the element has no placeholder attribute, skip it
        var placeholder = el.get('placeholder');
        if (!placeholder)
            return;

        // Store current element properties
        el.store('sfx:placeholder', this)
          .store('sfx:placeholder:text', placeholder)
          .erase('placeholder');

        // Bind the focus/blur events
        el.addEvents({
            focus: this.bound.onFocus,
            blur: this.bound.onBlur
        });

        // If there is no value, set the colour and default text
        if (!this.hasValue(el))
            this.restorePlaceholder(el);

        // Attach form submit handler
        if (this.options.clearOnSubmit) {
            var form = el.getParent('form');
            if (form && !form.retrieve('sfx:placeholder:form-handler')) {
                form.addEvent('submit', function(e) {
                    var placeholder_text;
                    var elements = $(e.target).getElements('input[type=text]');
                    for (i = 0; i < elements.length; i++) {
                         placeholder_text = elements[i].retrieve('sfx:placeholder');
                         if (placeholder_text && el.value == placeholder_text)
                            el.value = '';
                    }
                });
            }
        }
    },

    detach: function() {
        this.element.removeEvents({
            focus: this.bound.onFocus,
            blur: this.bound.onBlur
        });

        return this;
    },

    clearPlaceholder: function(el) {
        el.removeClass(this.options.className);
        if (el.value == el.retrieve('sfx:placeholder:text'))
            el.value = '';

        return this;
    },

    restorePlaceholder: function(el) {
        el.addClass(this.options.className)
          .value = el.retrieve('sfx:placeholder:text');

        return this;
    },

    onFocus: function(event) {
        this.clearPlaceholder($(event.target));
    },

    onBlur: function(event) {
        if (!this.hasValue(event.target))
            this.restorePlaceholder($(event.target));
    },

    hasValue: function(el) {
        return el.value.trim();
    }

});



SFx.Tabs = new Class({

    Implements: [Options],

    options: {
      tabs: [],
      content: null,        // The content container
      onSelect: $empty,     // Callback when tab selected
      onBeforeLoad: $empty, // Callback BEFORE ajax loading data
      type: 'swap',         // 'swap', 'load', 'custom',

      // AJAX content loading
      url: null,            // URL for AJAX load
      useSpinner: true,    // Use AJAX spinner on content element
      spinnerTarget: $empty // Alternative element object for spinner
    },

    initialize: function(options) {
        // Set class options
        this.setOptions(options);

        // Initialise tab elements
        this.tabs = $$(this.options.tabs);
        this.tabs.addEvent('click', this.onTabSelect.bindWithEvent(this));

        // Store the currently selected tab
        this.tabs.each(function(el) {
            if (el.hasClass('active'))
                this.selected_tab = el;
        }.bind(this));
    },

    onTabSelect: function(e) {
        this.selected_tab = $(e.target);
        var ref = Array.pick(Object.values(this.selected_tab.getProperties('sfx:tab', 'id', 'title'))) || '';

        // Update active tab selection
        this.tabs.removeClass('active');
        this.selected_tab.addClass('active');

        // Depending on the selection type, update the content
        switch (this.options.type) {
            case 'swap':
                // Hide the current content div
                $(this.options.content).getElements('.sfx-tab-content').hide();

                // Show the appropriate content div
                $(this.options.content).getElements('.sfx-tab-content[ref=' + ref + ']').show();
                break;

            case 'load':
                // If there is an onRequest method, call it to get the query string
                var data = this.options.onRequest(ref);
                if ($type(data) == 'object' || $type(data) == 'hash')
                        data = Hash.toQueryString(data);
                data = 'tab=' + ref + (data != '' ? '&' + data : '');

                // Call pre-load function
                this.options.onBeforeLoad();

                // AJAX load the contents
                new Request.HTML({
                  method: 'get',
                  url: this.options.url,
                  data: data,
                  update: $(this.options.content),
                  useSpinner: this.options.useSpinner,
                  spinnerTarget: ($empty(this.options.spinnerTarget) ? $(this.options.content) : this.options.spinnerTarget),
                  onSuccess: this.options.onSelect.pass([this, ref])
                }).send();
                break;

            case 'custom':
                this.options.onSelect(this.selected_tab, ref);
                break;
        }
    },

    select: function(tab) {
        if (!tab)
            this.tabs.each(function(el) { if (el.hasClass('active')) { tab = el; } });

        var index = ($type(tab) == 'element') ? this.tabs.indexOf(tab) : tab;
        var el = this.tabs[index];
        el.fireEvent('click', new Event.Mock(el));
    }

});


Element.implement('valign', function() {
    // Calculate the height of the container element
    var top_padding = (this.getParent().getSize().y - this.getSize().y) / 2;
    var container = new Element('div', {
      styles: { 'padding-top': top_padding + 'px' }
    }).wraps(this);
});


SFx.Overlay = new Class({
    Extends: Mask,

    Implements: [ Chain ],

    Binds: [ 'onShow', 'onHide' ],

    options: {
      'class': 'sfx-overlay',
      'style': {
        'opacity': 0
      }
    },

    initialize: function(options) {
        options = Object.merge({
          onShow: this.onShow,
          onHide: this.onHide,
          id: 'sfx-overlay-' + String.uniqueID()}, options);

        this.parent(document.body, options);
    },

    onShow: function() {
        this.element
          .set('tween', { duration: 100 })
          .fade(0.8)
          .get('tween').chain(function() {
            this.onShowComplete();
          }.bind(this))
    },

    hideParent: function() {
        //
        // The code below is copied directly from mootools-more's Overlay class
        // It will need to be updated along with any Mootools more updates.
        //
        if (this.hidden) return this;

        window.removeEvent('resize', this.position);
        this.hideMask.apply(this, arguments);
        if (this.options.destroyOnHide) return this.destroy();

        return this;
    },

    hide: function() {
        //
        // Prevent hiding when already hidden - e.g. when called from
        // the .destroy() method after already being hidden
        //
        if (this.hidden)
            return this;

        this.element
          .get('tween').chain(function() {
            // Hack to let the parent method be called
            this.hideParent();

            // Fire next event in chain
            this.onHideComplete();
          }.bind(this));
        this.element.fade(0);
    },

    onShowComplete: function() {
        this.callChain();
    },

    onHideComplete: function() {
        this.callChain();
    }
});

SFx.Dialog = new Class({
    Implements: [Options, Events, Chain],

    options: {
      width: 'auto',
      height: 'auto',
      title: 'Dialog',
      buttons: [],
      content: '<p>There is no content for this dialog.</p>',
      zIndex: 10,
      overlay: true,
      useForm: true,
      className: 'sfx-dialog',
      beforeSaveCheck: Function.from(true),
      afterSave: 'reload', /* reload, function */

      keys: {
        esc: function() { this.onCancel(); }
      }
      /*,

      onOpen: $empty,
      onClose: $empty
      */
    },

    initialize: function(options) {
        this.setOptions(options);

        // Default form submit value
        this.options.action = this.options.action || new URI().set('fragment', '').toString();

        // Store the title value for later
        this.options.origTitle = this.options.title;

        // Initialise the list of button elements
        this.buttons = [];
    },

    render: function() {
        // Modal overlay
        this.overlay = new SFx.Overlay();

        // Overall dialog container
        this.dialog = new Element('div', {
          'class': this.options.className,
          'styles': {
            'z-index': this.options.zIndex,
            'visibility': 'hidden',
            'left': 0,
            'top': 0
          }
        }).inject(document.body);

        // Dialog titlebar element
        this.titlebar = new Element('div', {
          'class': 'sfx-dialog-titlebar'
        }).inject(this.dialog);

        // Dialog title element
        this.title = new Element('span', {
          'class': 'sfx-dialog-title',
          'html': this.options.title
        }).inject(this.titlebar);

        // Dialog content container
        this.content = new Element('div', {
          'class': 'sfx-dialog-content',
          'styles': {
            'width':  (this.options.width == 'auto'  ? '450px' : this.options.width  + 'em'),
            'height': (this.options.height == 'auto' ? '300px' : this.options.height + 'em')
          },
          'html': this.options.content
        });

        // Dialog form wrapper
        if (this.options.useForm) {
            this.form = new Element('form', {
              'method': 'post',
              'action': this.options.action,
              'events': {
                'submit': function(e) { e.preventDefault(); }
              }
            }).injectInside(this.dialog);
            this.content.inject(this.form);
        } else {
            this.content.inject(this.dialog);
        }

        // Dialog button row
        this.button_container = new Element('div', {
          'class': 'sfx-dialog-buttons'
        }).inject(this.dialog);

        if (this.options.buttons.length) {
            //
            // Add the requested buttons
            //

            // The OK button should function as Cancel if there is only one
            // button in this dialog - check if this is the case
            if (this.options.buttons.length == 1
              && typeOf(this.options.buttons[0]) == 'string'
              && this.options.buttons[0].toUpperCase() == 'OK') {
                // OK button acts as cancel
                this.addButton(this.options.buttons[0], this.onCancel.bind(this));
            } else {
                // Individual button settings
                this.options.buttons.each(function(button) {
                    if (typeOf(button) === 'object')
                        this.addButton(button.name, button.onClick, button.className, button.enabled != undefined ? button.enabled : true);
                    else
                        this.addButton(button);
                }.bind(this));
            }
        } else {
            // Add default Save/Cancel buttons
            this.addButton('Save')
                .addButton('Cancel');
        }

        // Attach a drag handler to the titlebar
        this.drag = new Drag.Move(this.dialog, {
          handle: this.titlebar
        });
    },

    addButton: function(title, click_handler, class_name, enabled) {
        // List of default button handlers
        var default_buttons = {
          'Save':   this.onSave.bind(this),
          'Send':   this.onSave.bind(this),
          'OK':     this.onSave.bind(this),
          'Print':  this.onSave.bind(this),
          'Delete': this.onDelete.bind(this),
          'Cancel': this.onCancel.bind(this)
        };

        // Normalise the text for "Ok" to "OK"
        if (title == 'Ok')
            title = 'OK';

        // Ensure there is a click handler bound for the button
        if (typeof click_handler == 'string')
            click_handler = default_buttons[click_handler] || null;
        if (typeof click_handler == 'undefined') {
            click_handler = default_buttons[title] ||
              function() { alert('No action has been defined for this button'); };
        }

        // Use default class if none specified
        if (typeof class_name == 'undefined')
            class_name = 'sfx-button-' + title.toLowerCase();

        // Create the button element
        this.buttons[title] = (new Element('input', {
          'type': 'button',
          'value': title,
          'class': ('sfx-button ' + (class_name || '')).trim(),
          'disabled': typeof enabled == 'undefined' ? false : !enabled,
          events: {
            click: (click_handler || this.close).bind(this)
          }}).inject(this.button_container));

          return this;
    },

    disableButton: function(id) {
        if (typeof this.buttons[id] != 'undefined')
            this.buttons[id].disabled = true;
    },

    enableButton: function(id) {
        if (typeof this.buttons[id] != 'undefined')
            this.buttons[id].disabled = false;
    },

    destroy: function() {
        if (this.options.overlay)
            this.overlay.destroy();
        document.removeEvent('keyup', this.keyEvent);
        this.dialog.destroy();
    },

    show: function(args) {
        // Apply any title prefix
        if (args && args.prefix) {
            this.options.title = args.prefix + this.options.origTitle;
            delete args['prefix'];
        }

        this.render();
        this._show();
    },

    _show: function() {
        // Create the elements
        if (!this.isOpen) {
        }

        // Display the modal overlay
        if (this.options.overlay) {
            // Add our show method to the chain then fade in the overlay
            this.overlay.chain(
              this.showDialog.bind(this)
            ).show();
        } else {
            // Show the dialog
            this.showDialog();
        }
    },

    close: function() {
        // Close any open calendar dialogs
        // XXX this is a hack and doesn't belogn here
        try {
            deInitCalendar(false);
        } catch (e) { }

        // Hide the dialog
        this.dialog.hide();

        // Fade out the overlay and destroy it once it's hidden
        this.overlay.chain(function() {
            this.destroy()
        }.bind(this)).hide();

    },

    showDialog: function() {
        // Adjust the positioning of the dialog
        this._position();

        // Display the dialog element
        this.dialog.setStyle('visibility', 'visible');
        this.keyEvent = this.onKeyEvent.bindWithEvent(this);
        document.addEvent('keyup', this.keyEvent);

        //
        // Center any "loading" text that may still be present, otherwise
        // focus the first input element in the content area
        //
        var loading = this.content.getElement('.sfx-dialog-content-loading');
        if (loading) {
            loading.valign();
        } else {
            this.focusFirstInput();
        }

    },

    onShow: function() {

    },


    //
    // Default button actions
    //
    onSave: function() {
        // Validate data is correct
        if (!this.options.beforeSaveCheck())
            return false;

        // Disable all buttons while we are saving
        this.button_container.getElements('input').set('disabled', true);

        // Hide the content div and show our save-in-progress DIV
        this.content.hide();
        this.content_saving = new Element('div', {
          'class': 'sfx-dialog-content sfx-dialog-content-loading',
          'styles': {
            'width':  (this.options.width == 'auto'  ? '550px' : this.options.width  + 'em'),
            'height': (this.options.height == 'auto' ? '400px' : this.options.height + 'em')
          },
          'html': '<div>' +
                  '  <img src="/_common/images/elements/loading-segments.gif" width="31" height="31" alt=""> ' +
                  '  <span>Saving, one moment...</span>' +
                  '</div>'
        }).inject(this.content, 'before');
        this.content_saving.getElement('div').valign();

        // Handle save action
        if (typeOf(this.options.saveCallback) == 'function') {
            if (this.options.saveCallback((this.form || this.content.getElement('form')).getValues()))
                this.onSaveComplete('OK');
            else
                this.onSaveFailed();
        } else {
            // Submit the form data via an AJAX POST request
            this.saveRequest = new Request({
                method:     'post',
                url:        this.form.getProperty('action'),
                data:       this.form.toQueryString(),
                onSuccess: this.onSaveComplete.bind(this),
                onFailure:  this.onSaveFailed.bind(this)
            }).send();
        }
    },

    onSaveComplete: function(responseText) {
        // Grab any headers from the response
        this.returnHeaders = {};
        if (typeof this.saveRequest !== 'undefined') {
            this.returnHeaders = [];
            this.saveRequest.getAllResponseHeaders().trim().split("\n").each(function(header) {
                var hdr = header.split(':', 2);
                this.returnHeaders[hdr[0].trim()] = hdr[1].trim();
            }.bind(this));
        }

        // Check for a JSON MIME type - if found, ... use the force.
        var response = responseText;
        if (this.returnHeaders['Content-Type'] == 'application/json') {
            try {
                // Decode the JSON response
                response = JSON.decode(responseText);

                // Store the JSON object so other methods can access it
                this.saveResponse = response;
            } catch(e) { }
        }

        // Legacy behaviour: if the response text is not "OK",
        // fall into the error failure case
        if ((typeof response == 'object' && response.status != 'OK')
          || (typeof response != 'object' && response != 'OK')) {
            this.onSaveFailed(response);
            return;
        }

        // Display save successful notification
        this.content_saving.getElement('div').set('html',
          '<img src="/_common/images/icons/symbols/check24.png" width="24" height="24" alt=""> ' +
          '<span>Save successful</span>');

        // The save
        if (typeOf(this.options.afterSave) === 'function') {
            //
            // User-defined callback function
            //
            this.options.afterSave((this.form || this.content.getElement('form')).getValues(), this.saveResponse || {});

        } else if (this.options.afterSave === 'reload') {
            //
            // Reload the page when saving the dialog (default behaviour)
            //
            this.reloadPage.delay(500, this);
            return;
        }

        //
        // Close the dialog
        //
        this.close.delay(500, this);
    },

    onSaveFailed: function(response) {
        // Display error notification
        var failure_text = typeof response.message != 'undefined' ? response.message : response;
        if (failure_text == 'ERROR' || failure_text == 'FAIL') {
            failure_text =
              'Please ensure all required fields are completed and of the appropriate type for each field, then try again.' +
              'If this problem persits, please contact the Sentral Education Helpdesk for assistance.';
        }

        // Display generic error notification text
        this.content_saving.set('html',
          '<div>' +
          '  <img src="/_common/images/icons/symbols/error24.png" width="24" height="24" alt=""> ' +
          '  <span class="bold">An error occurred while saving</span>' +
          '  <p>' + failure_text + '</p>' +
          '  <p><a class="sfx-dialog-error-back-link pointer">Click here to go back</a></p>' +
          '</div>');
        this.content_saving.getElement('div').valign();

        // Attach the click event for the back button
        this.content_saving.getElement('a.sfx-dialog-error-back-link').addEvent('click', this.onSaveFailedBack.bind(this));
    },

    onSaveFailedBack: function() {
        // Remove the failure error text
        this.content_saving.dispose();

        // If there is any delete input, remove it so that if we fix the
        // problem and save, it won't still delete the item...
        if (typeof this.delete_input != 'undefined') this.delete_input.dispose();

        // Re-display the content
        this.content.show();

        // Re-enable the save controls
        this.button_container.getElements('input').set('disabled', false);
    },

    onCancel: function() {
        this.close();
    },

    onDelete: function() {
        if (confirm('Are you sure you wish to remove this entry?')) {
            this.delete_input  = new Element('input',
              { 'type': 'hidden',
                'name': 'modalDelete',
                'value': 'delete' }).injectInside(this.form);
            this.onSave();
        }
    },

    onKeyEvent: function(e) {
        if (this.options.keys[e.key]) {
            this.options.keys[e.key].call(this);
        }
    },

    _position: function() {
        var window_size = window.getSize();
        var scroll_size = window.getScroll();
        var dialog_size = this.dialog.getSize();

        this.dialog.setStyles({
          left: scroll_size.x + ((window_size.x - dialog_size.x) / 2),
          top:  scroll_size.y + ((window_size.y - dialog_size.y) / 2)
        });

        return this;
    },

    reloadPage: function() {
        // Reload the page by setting the URL, to avoid warnings about
        // resubmiting any page POST data. We remove the anchor as otherwise
        // the browser tries to navigate within the current page and no
        // reload of the page ever happens!
        var url = window.location.href.split('#')[0];

        // Call any user-specified reload handler, which gives consumers
        // the opportunity to modify the URL that will be loaded
//        var new_url = this.options.onPageReload(url);
//        url = new_url ? new_url : url;

        // Reload the specified URL
        window.location.href = url;
    },

    focusFirstInput: function() {
        var input_elements = this.content.getElements('input, select');
        for (var i = 0; i < input_elements.length; i++) {
            var el = input_elements[i];
            if (el.get('type') != 'hidden') {
                //
                // Focus the element then stop looking - however wrap this in
                // a try/catch block in case it fails for any reason.
                //
                try { el.focus(); } catch(e) { };
                break;
            }
        };
    }

});


SFx.Dialog.Request = new Class({
    Extends: SFx.Dialog,

    options: {
      method: 'get'
    },

    show: function(args) {
        // Set the content to display a loading spinner
        this.options.content =
          '<div class="sfx-dialog-content-loading">' +
          '  <img src="/_common/images/elements/loading-segments.gif" width="31" height="31" alt=""> ' +
          '  <span>Loading, please wait...</span>' +
          '</div>';

        // Set the dialog title prefix if required
        if (args && args.prefix) {
            this.options.title = args.prefix + this.options.origTitle;
            delete args['prefix'];
        }

        // Create the dialog and related elements
        this.render();

        // Load the AJAX content
        this.request = new Request.HTML({
          url: this.options.url,
          method: this.options.method,
          data: args || {},
          update: this.content,
          evalScripts: true,
          onComplete: this.focusFirstInput.bind(this)
        }).send();

        // Display the dialog
        this._show();
    }
});


//
// ModalDialog2 compatibility layer
//
ModalDialog2 = new Class({
    Extends: SFx.Dialog.Request,

    initialize: function(options) {
        var default_options = {
          heading: 'Dialog',
          action: '',
          url: null,
          width: 'auto',
          height: 'auto',
          reloadOnSave: true,
          cancelButton: 'Cancel',
          saveButton: 'Save',
          saveEnabled: true,
          deleteButton: false,
          saveCallback: null,
          saveCheck: Function.from(true),
          modalForm: true,
          method: 'get'
        };
        var options = Object.merge(default_options, options);

        //
        // Translate the list of buttons to display
        //
        var buttons = [];
        if (options.saveButton && options.saveButton != 'None') {
            buttons.push({
              name: options.saveButton,
              onClick: 'Save',
              enabled: options.saveEnabled
            });
        }
        if (options.cancelButton && options.cancelButton != 'None') {
            buttons.push({
              name: options.cancelButton,
              onClick: 'Cancel'
            });
        }
        if (options.deleteButton) {
            buttons.push('Delete');
        }

        //
        // Map options to their equivalent SFx.Dialog options
        //
        var sfx_options = {
          title:        options.heading,

          url:          options.url,
          action:       options.action || null,
          loadMethod:   options.method,

          height:       options.height,
          width:        options.width,

          buttons:      buttons,
          useForm:      options.modalForm,

          beforeSaveCheck: options.saveCheck,
          saveCallback: options.saveCallback || null,

          afterSave:    options.saveComplete || (options.reloadOnSave ? 'reload' : 'close')
        };

        // Call the parent constructor
        this.parent(sfx_options);
    },

    add: function(args) {
        this.show(Object.merge({prefix: 'Add '}, args));
    },

    edit: function(args) {
        this.show(Object.merge({prefix: 'Edit '}, args));
    },

    copy: function(args) {
        this.show(Object.merge({prefix: 'Copy '}, args));
    }
});


//
// Extend the string class with some dialog helpers
//
String.implement({
    alert: function(options) {
        var d = new SFx.Dialog(Object.merge(
          { title: 'Sentral Notification',
            buttons: [ { name: 'OK', onClick: 'Cancel' } ],
            content: this },
          options));
        d.show();
    }

});

//
// Extend the element class with some dialog helper functions
//
Element.implement({
    dialog: function(options) {
        // Hide the existing element
        this.hide();

        //
        // Dialog the element's contents in the dialog (note that any id's
        // will end up duplicated so you are best not to assign IDs in the
        // dialog contents)
        //
        var d = new SFx.Dialog(Object.merge(
          { title: 'Sentral Dialog',
            buttons: [ { name: 'OK', onClick: 'Cancel' } ],
            content: this.get('html') },
          options));
        d.show();
    }
});


//
// SFx.CheckboxToggle
//   Manages mass select/unselect/toggle functionality for groups of checkboxes
//
SFx.CheckboxToggle = new Class({
    Implements: [Options],

    options: {
        toggle: null
    },

    // The list of elements to toggle on/off
    elements: null,

    initialize: function(elements, options) {
        this.setOptions(options);

        // The list of checkboxes to toggle
        this.elements = $$(elements);

        // The element(s) that control the toggle
        if ($defined(this.options.toggle)) {
            if ($(this.options.toggle).getProperty('type') == 'checkbox') {
                $(this.options.toggle).addEvent('click', function() {
                    this.select($(this.options.toggle).get('checked'));
                }.bind(this));
            } else {
                $(this.options.toggle).addEvent('click', function() {
                    this.selectButton();
                }.bind(this));
            }
        }

        // Element to toggle the items on
        if ($defined(this.options.toggleOn)) {
            $(this.options.toggleOn).addEvent('click', this.selectAll.bind(this));
        }

        // Element to toggle the items off
        if ($defined(this.options.toggleOff)) {
            $(this.options.toggleOff).addEvent('click', this.deselectAll.bind(this));
        }
    },

    select: function(state) {
        $$(this.elements).each(function(el) {
            if (el != $(this.options.toggle)) {
                el.set('checked', state);
                el.fireEvent('click', new Event.Mock(el));
            }
        }.bind(this));
    },

    selectButton: function() {
        // First get the total checked and total unchecked
        // to decide which behaviour to use
        var checked = 0;
        var unchecked = 0;
        $$(this.elements).each(function(el) {
            if (el.checked)
                checked++;
            else
                unchecked++;
        });

        var behaviour = (checked <= unchecked);
        $$(this.elements).each(function(el) {
            el.checked = behaviour;
        });
    },

    selectAll: function() {
        this.select(true);
    },

    deselectAll: function() {
        this.select(false);
    }
});

SFx.Search = new Class({
    Implements: [Options],

    options: {
        url     :    '',
        delay   :  150,
        filters : null
    },

    initialize: function(element, options) {
        // Set options
        this.setOptions(options);

        // Store the element to attach to
        this.element = $(element);

        // Attach placeholder for pre-HTML5 browsers
        new SFx.Placeholder({ elements: this.element });

        // Create a search indicator
        this.search_indicator = new Element('img', {
          'src': '/_common/images/elements/loading-ball.gif',
          'width': 16,
          'height': 16,
          'styles': {
            'display': 'none'
          }
        }).inject($('bc-search'), 'top');

        // Attach the event handlers
        this.attach();
    },

    attach: function() {
        // Add the key events to the input element
        $(this.element).addEvents({
          keydown:  this.onNavigate.bindWithEvent(this),
          keyup:    this.onInput.bindWithEvent(this),
          blur:     function() { this.cancel.delay(150, this); }.bind(this)
        });
    },

    onNavigate: function(event) {
        if (!this.results)
            return;

        // Handles the keyboard navigation between results
        switch (event.key) {
            case 'up':
                //
                // Up Arrow
                //

                // Select the previous element on the list
                var previous_element = this.results.getElements('tr.sfx-search-result').getLast();
                var selected_element = this.results.getElement('tr.sfx-search-result.selected');

                if (selected_element != null) {
                    // We already have a selected element
                    selected_element.removeClass('selected');

                    if (selected_element.getPrevious('tr.sfx-search-result') != null)
                        previous_element = selected_element.getPrevious('tr.sfx-search-result');
                };

                if (previous_element != null)
                    previous_element.addClass('selected');

                break;

            case 'down':
                //
                // Down Arrow
                //

                // Select the next element on the list
                var next_element = this.results.getElement('tr.sfx-search-result');
                var selected_element = this.results.getElement('tr.sfx-search-result.selected');

                if (selected_element != null) {
                    // We already have a selected element
                    selected_element.removeClass('selected');

                    if (selected_element.getNext('tr.sfx-search-result') != null)
                        next_element = selected_element.getNext('tr.sfx-search-result');
                };

                if (next_element != null)
                    next_element.addClass('selected');

                break;

            case 'enter':
                //
                // Enter
                //

                // Simulate a mouse click on the selected element
                var selected_element = this.results.getElement('tr.sfx-search-result.selected');

                if (selected_element != null)
                    selected_element.fireEvent('mousedown');

                return false;
                break;
        }
    },

    onInput: function(event) {
        //
        // The following are valid reasons for not executing a search...
        //

        if ($(this.options.filters) != null) {
            $(this.options.filters).style.display = 'none';
        }

        // Escape - cancel the search field
        if (event.key == 'esc') {
            this.cancel();
            return;
        }

        // If the search input has not changed, just ignore the search request
        if (this.last_search == this.element.value)
            return;


        //
        // Proceed with searching
        //

        // Store the current searched value
        this.last_search = this.element.value;

        // Display the search indicator
        this.search_indicator.show('inline');

        // Call the search AJAX request after a specified timeout
        clearTimeout(this.search_timer);
        this.search_timer = this.search.delay(this.options.delay, this);
    },

    search: function() {
        // Only search if there is data to search on
        if (this.element.value) {
            // Cancel an existing request if active
            if (this.request)
                this.request.cancel();

            if (this.options.filters && $(this.options.filters)) {
                var filter_options = $(this.options.filters).toQueryString();
            }

            // Start a request
            this.request = new Request.JSON({
                url:    this.options.url,
                data:   'q=' + this.element.value + (filter_options ? '&' + filter_options : ''),
                method: 'get',
                onSuccess: this.renderResults.bind(this),
                onFailure: this.searchFailed.bind(this)
            }).send();
        } else {
            // Clear any existing search
            if (this.results) {
                this.results.dispose();
                this.results = null;
            }

            // Hide any active search indicator
            this.search_indicator.hide();
        }
    },

    cancel: function() {
        // Clear any search timers
        clearTimeout(this.search_timer);

        // Cancel any pending requests
        if (this.request)
            this.request.cancel();

        // Remove any search results box
        if (this.results) {
            this.results.dispose();
            this.results = null;
        }

        // Hide the search indicator
        this.search_indicator.hide();

        // Reset the last searched text
        this.last_search = '';

        // Clear any existing search text
        this.element.set('value', '');
    },

    searchFailed: function() {
        // Remove any previous results
        if (this.results) {
            this.results.dispose();
            this.results = null;
        }

        // Hide the search indicator
        this.search_indicator.hide();

        // Create new results container
        this.results = new Element('div', {
          'class': 'sfx-search-results'
        });

        // Display the results
        // An error occured, inform the user
        this.results
          .set('html', '<p class="sfx-search-error"><img src="/_common/images/icons/symbols/error16.png" width="16" height="16"> An error occurred while searching.</p>')
          .inject(this.element, 'after');
    },

    renderResults: function(response) {
        // Remove any previous results
        if (this.results) {
            this.results.dispose();
            this.results = null;
        }

        // Hide the search indicator
        this.search_indicator.hide();

        // Create new results container
        this.results = new Element('div', {
          'class': 'sfx-search-results'
        });

        // Display the no results message (where there are no results)
        if (!response.has_results) {
            this.results
              .set('html', '<p class="sfx-search-no-results"><img src="/_common/images/icons/symbols/bulb16.png" width="16" height="16"> No search results found.</p>')
              .inject(this.element, 'after');
            return;
        }

        // Display the results
        if (!response.sections) {
            // An error occured, inform the user
            this.results
              .set('html', '<p class="sfx-search-error"><img src="/_common/images/icons/symbols/error16.png" width="16" height="16"> An error occurred while searching.</p>')
              .inject(this.element, 'after');
            return;
        }

        // Build the results display
        var table = new Element('table').inject(this.results);
        var table_body = new Element('tbody').inject(table);

        response.sections.each(function(section) {
            //
            // Display section headings
            //
            if (section.heading != '') {
                var heading_row  = new Element('tr', { 'class': 'sfx-search-results-heading' }).inject(table_body);
                var heading_cell = new Element('th', { 'colspan': 3, 'html': section.heading }).inject(heading_row);
            }

            //
            // Display search results rows
            //
            section.results.each(function(result) {
                // Create a new row for the result
                var table_row = new Element('tr', { 'class': 'sfx-search-result' }).inject(table_body);
                if (result.link)
                    table_row.addEvent('mousedown', function() { window.location.href = result.link; } );
                else
                    table_row.addEvent('click', function() { alert('click'); });

                // Create the icon (if required)
                var result_icon_cell = new Element('td', { 'class': 'icon' });
                if (result.icon) {
                    new Element('img', {
                      'src': result.icon || '',
                      'height': 16,
                      'width': 16
                    }).inject(result_icon_cell);
                }
                result_icon_cell.inject(table_row);

                // Create the title element
                new Element('td', { 'class': 'title', 'html': result.title }).inject(table_row);

                // Create the description element
                new Element('td', { 'class': 'description', 'html': result.description || '' }).inject(table_row);
            }.bind(this));
        }.bind(this));

        // Insert the results display
        this.results.inject(this.element, 'after');

/*        if (response.more_results) {
            // This search returns more rows
            var table_row = new Element('tr', {
                    'class'     : 'more_results'
                }).inject(table_body);

            var table_data = new Element('td', {
                    'colspan'   : 3
                }).inject(table_row);
            table_data.appendText('More results found, please refine the search');
        }

        if (response.results.length == 0) {
            // No results found
            var table_row = new Element('tr', {
                    'class' : 'no_result'
                }).inject(table_body);

            var table_data = new Element('td').injectInside(table_row);
            table_data.appendText('No results found.');
        }

        if (this.auto_select && response.results.length == 1) {
            // Auto select the only result
            this.elements.results_container.hide();
            this.options.select_result.attempt(response.results[0].id);
        }*/
    }
});

SFx.Grid = new Class({
    Implements: [ Options ],

    Binds: [
        'initialize',
        'onKeyEnter',
        'onKeyTab',
        'onKeyControlEnter',
        'onKeyShiftTab',
        'getLastCol',
        'getLastRow',
        'getTabNext',
        'getEnterNext',
        'getTabPrevious',
        'getEnterPrevious',
        'getCurrentCol',
        'getCurrentRow',
        'getCell'
    ],

    options: {
        grid: 'table.grid',
        inputs: 'input.edit',
        activeStyle: 'active'
    },

    // last row and col
    // use getLastRow() and getLastCol() functions to get these values
    last_col: null,
    last_row: null,

    initialize: function(options) {
        this.setOptions(options);

        // prevent tab, enter and other key presses from triggering their default events
        window.addEvent('keypress', function(event) { this.preventDefault(event) }.bind(this));
        window.addEvent('keyup', function(event) { this.preventDefault(event) }.bind(this));
        window.addEvent('keydown', function(event) { this.preventDefault(event) }.bind(this));

        // use keyboard object
        this.keyboard = new Keyboard({
            active: true,
            defaultEventType: 'keydown',
            events: {
                'tab': this.onKeyTab,
                'right': this.onKeyTab,
                'enter': this.onKeyEnter,
                'down': this.onKeyEnter,
                'shift+tab': this.onKeyShiftTab,
                'left': this.onKeyShiftTab,
                'ctrl+enter': this.onKeyControlEnter,
                'up': this.onKeyControlEnter
            }
        });

        // enable single click select all
        $$(this.options.inputs).addEvent('click', function(event) {
            event.target.getParent('table.grid').getElements('div.active').removeClass('active');
            event.target.select();
            event.target.getParent('div').addClass('active');
        });

        // place focus in the first edit input and select any data within
        var first_edit = $$(this.options.grid).getElement(this.options.inputs).pick();
        first_edit.focus();
        first_edit.select();
        first_edit.getParent('div').addClass('active');
    },

    preventDefault: function(event) {
        if (event.key == 'down' || event.key == 'enter' || event.key == 'up' || event.code == 9 ||
            event.key == 'right' || (event.shift && event.code == 9) || event.key == 'left' || (event.control && event.key == 'enter')) {
            event.preventDefault();
        }
    },

    onKeyEnter: function(event) {
        event.preventDefault();

        // remove cell edit style
        event.target.getParent('div').removeClass(this.options.activeStyle)
        // get current column and row
        var cell = this.getEnterNext(event.target);
        if (cell != null) {
            cell.focus();
            cell.select();
            // apply cell edit style
            cell.getParent('div').addClass(this.options.activeStyle)
        }
    },

    onKeyControlEnter: function(event) {
        event.preventDefault();

        // remove cell edit style
        event.target.getParent('div').removeClass(this.options.activeStyle)
        // get current column and row
        var cell = this.getEnterPrevious(event.target);
        if (cell != null) {
            cell.focus();
            cell.select();
            // apply cell edit style
            cell.getParent('div').addClass(this.options.activeStyle)
        }
    },

    onKeyTab: function(event) {
        event.preventDefault();

        // remove cell edit style
        event.target.getParent('div').removeClass(this.options.activeStyle)
        // get current column and row
        var cell = this.getTabNext(event.target);
        if (cell != null) {
            cell.focus();
            cell.select();
            // apply cell edit style
            cell.getParent('div').addClass(this.options.activeStyle)
        }
    },

    onKeyShiftTab: function(event) {
        event.preventDefault();

        // remove cell edit style
        event.target.getParent('div').removeClass(this.options.activeStyle)
        // get current column and row
        var cell = this.getTabPrevious(event.target);
        if (cell != null) {
            cell.focus();
            cell.select();
            // apply cell edit style
            cell.getParent('div').addClass(this.options.activeStyle)
        }
    },

    getEnterNext: function(cell) {
        col = parseInt(this.getCurrentCol(cell));
        row = parseInt(this.getCurrentRow(cell));

        if (row >= this.getLastRow()) {
            row = 0;
            if (col >= this.getLastCol()) {
                col = 0;
            } else {
                col++;
            }
        } else {
            row++;
        }
        return this.getCell(cell, col, row);
    },

    getEnterPrevious: function(cell) {
        col = parseInt(this.getCurrentCol(cell));
        row = parseInt(this.getCurrentRow(cell));

        if (row <= 0) {
            row = this.getLastRow();
            if (col <= 0) {
                col = this.getLastCol();
            } else {
                col--;
            }
        } else {
            row--;
        }
        return this.getCell(cell, col, row);
    },

    getTabNext: function(cell) {
        col = parseInt(this.getCurrentCol(cell));
        row = parseInt(this.getCurrentRow(cell));

        if (col >= this.getLastCol()) {
            col = 0;
            if (row >= this.getLastRow()) {
                row = 0;
            } else {
                row++;
            }
        } else {
            col++;
        }
        return this.getCell(cell, col, row);
    },

    getTabPrevious: function(cell) {
        col = parseInt(this.getCurrentCol(cell));
        row = parseInt(this.getCurrentRow(cell));

        if (col <= 0) {
            col = this.getLastCol();
            if (row <= 0) {
                row = this.getLastRow();
            } else {
                row--;
            }
        } else {
            col--;
        }
        return this.getCell(cell, col, row);
    },

    getLastCol: function() {
        if (this.last_col === null) {
            inputs = $$(this.options.inputs);
            if (inputs != null) {
                this.last_col = parseInt(inputs.getLast().getProperty('data-col'));
            } else {
                this.last_col = 0;
            }
        }
        return this.last_col;
    },

    getLastRow: function() {
        if (this.last_row === null) {
            inputs = $$(this.options.inputs);
            if (inputs != null) {
                this.last_row = parseInt(inputs.getLast().getProperty('data-row'));
            } else {
                this.last_row = 0;
            }
        }
        return this.last_row;
    },

    getCurrentCol: function(cell) {
        return cell.getProperty('data-col');
    },

    getCurrentRow: function(cell) {
        return cell.getProperty('data-row');
    },

    applyStyle: function(cell) {
        cell.getParent('td').addClass('active');
    },

    removeStyle: function(cell) {
        cell.getParent('td').removeClass('active');
    },

    getCell: function(cell, col, row) {
        return cell.getParent('table').getElement(this.options.inputs + '[data-col=' + col + '][data-row=' + row + ']');
    }
});

SFx.ColsAlignment = new Class({
    Implements: [ Options ],

    options: {
        outer: 'outer-box',
        left: 'left-box',
        right: 'right-box',
        inner: 'inner-box',
        lock: 'left',
        gap: 0
    },
    h_measure_options: {
        mode: 'horizontal',
        styles: ['padding','border','margin']
    },
    v_measure_options: {
        mode: 'vertical',
        styles: ['padding','border','margin']
    },

    initialize: function(options) {

        this.setOptions(options);
        this.outer = $(this.options.outer);

        if (this.options.lock == 'left') {
            this.locked = $(this.options.left);
            this.resize_outer = $(this.options.right);
            this.resize_inner = $(this.options.inner);
        } else {
            this.locked = $(this.options.right);
            this.resize_outer = $(this.options.left);
            this.resize_inner = $(this.options.inner);
        }

        window.addEvent('resize', this.resize.bind(this));

        // do initial resize
        if (this.resize_outer != null) {
            this.setRowHeights();
            this.resize();
        }
    },

    resize: function() {
        // set the side to lock which determines the side to dynamically resize
        var outer_dimensions = this.outer.getComputedSize(this.h_measure_options);

        if (this.options.gap != 0) {
            this.locked.setStyle('padding-' + (this.options.lock ? 'right' : 'left'), this.options.gap);
        }
        // set margin
        var locked_dimensions = this.locked.getComputedSize(this.h_measure_options);
        var resize_outer_dim = this.resize_outer.getComputedSize(this.h_measure_options);
        var resize_inner_dim = this.resize_inner.getComputedSize(this.h_measure_options);
        var resize_excess = resize_outer_dim['computedLeft'] + resize_outer_dim['computedRight'] + resize_inner_dim['computedLeft'] + resize_inner_dim['computedRight'];
        // get total width including padding, borders, etc
        var inner_width = outer_dimensions['width'] - locked_dimensions['totalWidth'] - resize_excess - 1;

        if (inner_width < 0)
            inner_width = 0;

        this.resize_outer.setStyles({
            display: 'inline-block',
            width: inner_width
        });

        inner_width = this.resize_outer.getComputedSize(this.h_measure_options);
        // only set the max-width style otherwise it will stretch the content
        this.resize_inner.setStyle('max-width', inner_width['width']);
    },

    setRowHeights: function() {
        // get table elements for both left and right sides
        var table1 = this.locked.getElement('table');
        var table2 = this.resize_outer.getElement('table');

        // get the tr elements for each table
        var rows1 = table1.getElements('tr');
        var rows2 = table2.getElements('tr');

        if (rows1.length == rows2.length) {
            for ( var i = 0; i < rows1.length; i++) {
                // get largest height
                var height1 = rows1[i].getComputedSize(this.v_measure_options).totalHeight;
                var height2 = rows2[i].getComputedSize(this.v_measure_options).totalHeight;
                var height = Math.max(height1, height2);

                // set row heights to largest
                rows1[i].setStyle('height', parseInt(height) + 'px');
                rows2[i].setStyle('height', parseInt(height) + 'px');
            }
        }
    }
});



SFx.SpellCheck = new Class({
    Implements: [ Events, Options ],

    options: {
/*    onCorrect: $empty, */
      container: document.body,
      errorClassName: 'sfx-spelling-error',
      correctedClassName: 'sfx-spelling-correction'
    },

    initialize: function(options) {
        // Set options
        this.setOptions(options);

        // Initialise the list of stored corrections
        this.corrections =[];

        // Attach handler the comment
        this.attach($(this.options.container));
    },

    attach: function(el) {
        el.getElements('.' + this.options.errorClassName).each(function(el) {
            this.attachSpellingError(el);
        }.bind(this));
    },

    attachSpellingError: function(el) {
        // Pull the suggestions into an internal element store
        var suggestions = (el.get('title') || '').split(',');
        el.store('spellcheck:suggestions', suggestions).erase('title');

        // Move the offset and length information an internal element store
        el.store('spellcheck:offset', parseInt(el.get('data-position'))).erase('data-position');
        el.store('spellcheck:length', parseInt(el.get('data-level'))).erase('data-level');

        // Add a click handler for the suggestion
        el.addEvent('click', this.showSuggestions.bindWithEvent(this));

        // Add the element to our tracking list
        this.corrections.push(el);
    },

    showSuggestions: function(event) {
        // Prevent the click event from bubbling up to container elements
        event.stopPropagation();

        // If this suggestion DIV is open it, destroy it first
        this.hideSuggestions();

        // Create a transparent overlay to catch any clicks outside the menu
        this.overlay = new Element('div', {
          styles: {
            'background-color': 'transparent',
            'width': '100%',
            'height': '100%',
            'left': 0,
            'top': 0,
            'position': 'fixed'
          }
        }).inject(document.body)
          .addEvent('click', this.hideSuggestions.bind(this));

        // Build the list of suggestions
        var suggest_ul = new Element('ul');
        var suggestions = $(event.target).retrieve('spellcheck:suggestions');
        if (suggestions.length > 0) {
            // Add the suggestions to the list
            for (i = 0; i < suggestions.length; i++) {
                new Element('li', {
                  text: suggestions[i],
                  events: {
                    'click': this.selectSuggestion.bindWithEvent(this, [ event.target, suggestions[i] ])
                  }
                }).inject(suggest_ul);
            }
        } else {
            // No suggestions found
        }

        // Insert the list inside a container element
        SFx.SpellCheck.Suggestions = new Element('div', { 'class': 'sfx-spelling-suggestions' });
        suggest_ul.inject(SFx.SpellCheck.Suggestions);


        // Display the list of suggestions
        SFx.SpellCheck.Suggestions.inject(event.target, 'after');
        SFx.SpellCheck.Suggestions.position({
          relativeTo: event.target,
          position: 'bottomLeft',
          offset: { x: 0, y: 2 }
        });
    },

    hideSuggestions: function() {
        if (typeOf(this.overlay) != 'null') {
            this.overlay.destroy();
            this.overlay = null;
        }
        if (typeOf(SFx.SpellCheck.Suggestions) != 'null') {
            SFx.SpellCheck.Suggestions.destroy();
            SFx.SpellCheck.Suggestions = null;
        }
    },

    selectSuggestion: function(event, error_span, suggestion) {
        // Prevent the click event from bubbling up to container elements
        event.stopPropagation();

        // Hide the suggestions list
        this.hideSuggestions();

        // Remove the styles and click event from the error text
        error_span
          .removeEvent('click')
          .removeClass(this.options.errorClassName)
          .addClass(this.options.correctedClassName);

        // Find the offset and length of the word we are replacing
        var offset = $(error_span).retrieve('spellcheck:offset');
        var length = $(error_span).retrieve('spellcheck:length');

        // On the assumption that the substitution has been made, adjust the
        // offsets of all corrections that start after this one
        var offset_adjustment = length - suggestion.length;
        for (i = 0; i < this.corrections.length; i++) {
            var cur_offset = this.corrections[i].retrieve('spellcheck:offset');
            if (cur_offset > offset)
                this.corrections[i].store('spellcheck:offset', cur_offset - offset_adjustment);
        }

        // Fire the event so the
        this.fireEvent('correct', [ offset, length, suggestion, error_span ]);
    }

});


SFx.Expander = new Class({
    Implements: [ Events, Options ],

    options: {
/*    onOpen: $empty,
      onClose: $empty, */
      toggle: null,
      container: null,
      initialState: 'open'
    },

    initialize: function(options) {
        // Set options
        this.setOptions(options);

        // Attach the toggle events
        this.attach(this.options.toggle, this.options.container);
    },

    attach: function(toggle, container) {
        this.fx = new Fx.Slide(container, {
          onStart: this.onStart.bindWithEvent(this)
        });

        // Set the initial state to hidden if we need to
        if ($(container).getStyle('display') == 'none') {
            this.fx.hide();
            $(container).setStyle('display', '');
        }

        //

        // Attach the toggle
        $(toggle).addEvent('click', this.toggle.bindWithEvent(this));

    },

    onStart: function() {
        this.fireEvent('start', this.fx.open);
    },

    show: function() {
        this.fx.show();
    },

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

    toggle: function() {
        this.fx.toggle();
    },

    onOpen: function() {
        this.fireEvent('open');
    },
    onClose: function() {
        this.fireEvent('close');
    }
});

SFx.Expander.ContentBox = new Class({
    Extends: SFx.Expander,

    initialize: function(content_box, info_text) {
        // Set the toggle and container
        this.options.toggle = $(content_box).getElement('.content-box-header');
        this.options.container = $(content_box).getElement('.content-box-content');

        // Add the content box elements and styles
        this.content_box = $(content_box);
        this.content_box.addClass('expandable');
        new Element('div', { 'class': 'expander-info' })
          .set('text', info_text || 'Click to toggle contents')
          .inject(this.options.toggle, 'top');
        new Element('div', { 'class': 'expander' })
          .inject(this.options.toggle, 'top');

        // Set the current state style
        var current_state = this.options.container.getStyle('display') != 'none';
        $(this.content_box)
          .removeClass(current_state ? 'closed' : 'open')
          .addClass(current_state ? 'open' : 'closed');

        // Call parent class constructor
        this.parent();
    },

    onStart: function() {
        $(this.content_box)
          .removeClass(this.fx.open ? 'open' : 'closed')
          .addClass(this.fx.open ? 'closed' : 'open');
    }
});

SFx.Flash = new Class({
    Implements: [ Options ],

    options: {
        container: 'flash',
        duration: 3000,
        type: 'information',
        message: ''
    },

    initialize: function(options) {
        this.setOptions(options);

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

    build: function() {
        var notification = Elements.from('<div class="notification ' + this.options.type + '"><div>' + this.options.message + '</div></div>');

        if ($(this.options.container) !== null) {
            this.notification = notification.inject(this.options.container, 'bottom')[0];
            this.notification.hide();
        }
    },

    show: function() {
        // only show if we have a message to show
        if (this.options.message.length != '') {
            // and container actually exists
            if ($(this.options.container) !== null) {
                if (this.options.duration > 0) {
                    // show only for set duration
                    $(this.notification).wink(this.options.duration);
                } else {
                    // keep on screen
                    $(this.notification).reveal();
                }
            }
        }
    }
});

SFx.Collapse = new Class({});

SFx.Collapse.Sidebar = new Class({
    Implements: [ Options ],

    options: {
        trigger: '.sidebar-collapse'
    },

    initialize: function(collapse, options) {
        this.setOptions(options);

        collapse = collapse || false;

        // set initial state
        this.state = false;

        this.content = $('layout-3col-content-container') || $('layout-2col-r-content-container');
        this.sidebar = $('layout-3col-right') || $('layout-2col-right');

        sidebar_height = this.sidebar.getSize();

        this.sidebar.set('reveal', {
            mode: 'horizontal',
            heightOverride: sidebar_height.y
        });

        // only collapse if set to true
        if (collapse) {
            this.content.setStyle('margin-right', 0);
            this.sidebar.hide();

            // set the collapsed state
            this.state = true;
        }

        // setup trigger onclick events
        $$(this.options.trigger).addEvent('click', function(event) {
            this.cToggle();
        }.bind(this));
    },

    cToggle: function() {
        if (this.state) {
            this.expand();
        } else {
            this.collapse();
        }
    },

    collapse: function() {
        this.content.tween('margin-right', [240, 0]);
        this.sidebar.dissolve();

        // set the collapsed state
        this.state = true;
    },

    expand: function() {
        this.content.tween('margin-right', [0, 240]);
        this.sidebar.reveal();

        // set the collapsed state
        this.state = false;
    }
});

SFx.Collapse.Nav = new Class({
    Implements: [ Options, Chain ],

    options: {
        trigger: '.nav-collapse'
    },

    initialize: function(collapse, options) {
        this.setOptions(options);

        collapse = collapse || false;

        // set initial state
        this.state = false;

        this.layout = $('layout-2col') || $('layout-3col');
        this.footer = $('sentral-footer');
        this.content = $('layout-2col-content-container') || $('layout-3col-content-container');
        this.nav = $('layout-2col-left') || $('layout-3col-left');

        this.nav.set('reveal', {
            mode: 'horizontal'
        });

        // only collapse if set to true
        if (collapse) {
            this.content.setStyle('margin-left', 0);
            this.layout.removeClass('with-nav-menu');
            this.footer.removeClass('with-nav-menu');
            this.nav.hide();

            // set the collapsed state
            this.state = true;
        }

        // trigger the resize event for Sentral.NavMenu()
        window.fireEvent('resize');

        // setup trigger onclick events
        $$(this.options.trigger).addEvent('click', function(event) {
            this.cToggle();
        }.bind(this));
    },

    cToggle: function() {
        var toggleChain = new Chain();
        toggleChain.chain(
            function() {
                if (this.state) {
                    this.expand();
                } else {
                    this.collapse();
                }
            }.bind(this),
            function() {
                // trigger the resize event for Sentral.NavMenu()
                window.fireEvent('resize');
            }.bind(this)
        );
        toggleChain.callChain();
        toggleChain.callChain();
    },

    collapse: function() {
        this.content.tween('margin-left', [205, 0]);
        this.layout.removeClass('with-nav-menu');
        this.footer.removeClass('with-nav-menu');
        this.nav.dissolve();

        // set the collapsed state
        this.state = true;
    },

    expand: function() {
        this.content.tween('margin-left', [0, 205]);

        this.nav.reveal();
        this.layout.addClass('with-nav-menu');
        this.footer.addClass('with-nav-menu');

        // set the collapsed state
        this.state = false;
    }
});

