/* An empty class so we can create things inside it */
var Jifty = {};

// NOTICE: line break should be in a proper place.
//   something like
//          return
//          {
//              a : 123
//          };
//   would cause IE6,7 javascript error.
//   should write:
//          return {
//              a: 123
//          };

// Jifty.Update:
//   to add a response_hook
//   the handler will get two arguments:
//
//   response_fragment:
//       - response xml dom object
//
//   f:
//       - 'region' is the name of the region to update
//       - 'args' is a hash of arguments to override
//       - 'path' is the path of the fragment (if this is a new fragment)
//       - 'element' is the CSS selector of the element to update, if 'region' isn't supplied
//       - 'mode' is one of 'Replace', 'Top', 'Bottom', 'Before', or 'After'
//       - 'effect' is the name of an effect
//
Jifty.Update = {
    response_hooks: [],
    handler_hooks: [],
    addHook:function(f){
        this.response_hooks.push(f);
    }
};

Jifty.$ = function(id) {
    if (typeof id == 'string')
        return document.getElementById(id);
    return id;
};

Jifty.hasAjaxTransport = function() {
    var r = false;
    jQuery.each( [
            function() {return new XMLHttpRequest();},
            function() {return new ActiveXObject('Msxml2.XMLHTTP');},
            function() {return new ActiveXObject('Microsoft.XMLHTTP');}
        ], function(i, v) {
            try {
                r = v();
                if (r) return false;
            } catch(e) {}
        });
    return r ? true : false;
}();

Jifty.stopEvent = function(ev) {
    jQuery.event.fix(ev);
    ev.preventDefault();
    ev.stopPropagation();
};

Jifty.Web = {};
Jifty.Web.current_actions = [];

function register_action(a) {
    outs(div(function() {
                attr(function() {
                    return ['class', 'hidden'];
                });
                return input(function() {
                      attr(function() {
                            return ['type', 'hidden',
                                        'name', a.register_name(),
                                        'id', a.register_name(),
                                        'value', a.actionClass];
                      });
                });
            }));
    /* XXX: fallback values */
}

/* Actions */
var Action = function() {
    this.initialize.apply(this, arguments);
    return this;
};

Action.prototype = {
    // New takes the moniker (a string), and an optional array of form
    // elements to additionally take into consideration
    initialize: function(moniker) {
        this.moniker = moniker;

        // Extra form parameters
        this.extras = [];
        if (arguments.length > 1) {
            this.extras = arguments[1];
        }

        this.register = Jifty.$('J:A-' + this.moniker); // Simple case -- no ordering information
        if (! this.register) {
            var elements = [];
            // We need to go looking -- this also goes looking through this.extras, from above

            var add_to_elements = function(){ elements.push(this); };
            jQuery('input').each(add_to_elements);
            jQuery.each(this.extras, add_to_elements);

            for (var i = 0, l = elements.length; i < l; i++) {
                if ((Jifty.Form.Element.getMoniker(elements[i]) == this.moniker) && (Jifty.Form.Element.getType(elements[i]) == "registration")) {
                    this.register = elements[i];
                    break;
                }
            }
        }

        if (this.register) {
            this.form = Jifty.Form.Element.getForm(this.register);
            this.actionClass = this.register.value;
        }
    },

    // Returns an Array of all fields in this Action
    fields: function() {
        if(!this.cached_fields) {
            var elements = [];
            var possible = Jifty.Form.getElements(this.form);
            // Also pull from extra query parameters
            for (var i = 0, l = this.extras.length; i < l; i++)
                possible.push(this.extras[i]);

            for (var i = 0, l = possible.length; i < l; i++) {
                if (Jifty.Form.Element.getMoniker(possible[i]) == this.moniker)
                    elements.push(possible[i]);
            }
            this.cached_fields = elements;
        }
        return this.cached_fields;
    },

    buttons: function() {
        var elements = new Array();
        var possible = Jifty.Form.getElements(this.form);
        for(var i = 0; i < possible.length; i++) {
            if(possible[i].nodeName == 'INPUT' && possible[i].getAttribute("type") == 'submit') {
                actions = Jifty.Form.Element.buttonActions(possible[i]);
                //If the button has no actions explicitly associated
                //with it, it's associated with all the actions in the
                //form
                if( actions.length === 0
                   || actions.indexOf(this.moniker) >= 0) {
                    elements.push(possible[i]);
                }
            }
        }
        return elements;
    },

    getField: function(name) {
        var elements = this.fields();
        for (var i = 0; i < elements.length; i++) {
            if (Jifty.Form.Element.getField(elements[i]) == name)
                return elements[i];
        }
        return null;
    },

    // Serialize and return all fields needed for this action
    serialize: function() {
        var fields = this.fields();
        var serialized = new Array;

        jQuery.each(fields, function() {
            serialized.push( jQuery(this).serialize() )
        });

        return serialized.join('&');
    },

    // Returns true if there is a file upload form as one of our elements
    hasUpload: function() {
        var fields = this.fields();
        for (var i = 0, l = fields.length; i < l; i++) {
            if ((fields[i].getAttribute("type") == "file") && fields[i].value)
                return true;
        }
        return false;
    },

    // Return the action as a data structure suitable to be JSON'd
    data_structure: function() {
        var a = {};
        a['moniker'] = this.moniker;
        a['class']   = this.actionClass;

        if (this.register && this.register.id) {
            var tmp = this.register.id.match(/^J:A-(\d+)-/);
            if (tmp && tmp.length == 2)
                a['order'] = tmp[1];
        }

        a.fields  = {};
        var fields = this.fields();
        for (var i = 0; i < fields.length; i++) {
            var f = fields[i];

            if (   (Jifty.Form.Element.getType(f) != "registration")
                && (Jifty.Form.Element.getValue(f) !== null)
                && (!Jifty.Placeholder.hasPlaceholder(f)))
            {
                if (! a.fields[Jifty.Form.Element.getField(f)])
                    a.fields[Jifty.Form.Element.getField(f)] = {};
                var field = Jifty.Form.Element.getField(f);
                var type = Jifty.Form.Element.getType(f);

                // XXX: fallback value being an array makes server
                // upset, we don't think that should happen anyway
                if (type == 'fallback' && a.fields[field][type])
                    continue;

                a.fields[field][type] = this._mergeValues(a.fields[field][type],
                                                             Jifty.Form.Element.getValue(f));

            }
        }

        return a;
    },

    _mergeValues: function() {
        var oldval = arguments[0];
        var newval = arguments[1];
        if(!oldval) return newval;
        if(oldval.constructor != Array) {
            oldval = [oldval];
        }
        oldval.push(newval);
        return oldval;
    },

    // Validate the action
    validate: function() {
        show_wait_message();
        var id = this.register.id;

        var data = this.serialize();
        data += '&J:VALIDATE=1';
        if (this.form['J:C']) {
            data += '&J:C='+this.form['J:C'].value;
        }

        jQuery.ajax({
            url: '/__jifty/validator.xml',  // Right now, the URL is actually completely irrelevant
            type: "get",
            data: data,
            complete: function (request, status) {
                var response  = request.responseXML.documentElement;
                for (var action = response.firstChild; action !== null; action = action.nextSibling) {
                    if ((action.nodeName == 'validationaction') && (action.getAttribute("id") == id)) {
                        for (var field = action.firstChild; field !== null; field = field.nextSibling) {
                            // Possibilities for field.nodeName: it could be #text (whitespace),
                            // or 'blank' (the field was blank, don't mess with the error div), or 'ok'
                            // (clear the error and warning div!) or 'error' (fill in the error div, clear
                            // the warning div!) or 'warning' (fill in the warning div and clear the error div!)
                            if (field.nodeName == 'error' || field.nodeName == 'warning') {
                                var err_div = document.getElementById(field.getAttribute("id"));
                                if (err_div !== null) {
                                    jQuery(err_div).show().html(field.firstChild.data);
                                }
                            } else if (field.nodeName == 'ok') {
                                var err_div = document.getElementById(field.getAttribute("id"));
                                if (err_div !== null) {
                                    jQuery(err_div).hide().html('');
                                }
                            }
                        }
                    } else if ((action.nodeName == 'canonicalizeaction') && (action.getAttribute("id") == id)) {
                        for (var field = action.firstChild; field !== null; field = field.nextSibling) {
                            // Possibilities for field.nodeName: it could be 'ignored', 'blank' , 'update', or 'info'
                            // info is a separate action from the update
                            if (field.nodeName == 'canonicalization_note')  {
                                var note_div= document.getElementById(field.getAttribute("id"));
                                if (note_div !== null) {
                                    jQuery(note_div).show().html(field.firstChild.data);
                                }
                            }

                            if (field.nodeName == 'update') {
                                var field_name = field.getAttribute("name");
                                for (var form_number = 0 ; form_number < document.forms.length; form_number++) {
                                    var form_field = document.forms[form_number].elements[field_name];
                                    if (form_field  == null || !jQuery(form_field).is('.ajaxcanonicalization'))
                                        continue;
                                    form_field.value = field.firstChild.data;
                                }
                            }
                        }
                    }
                }
                return true;
            }
        });

        hide_wait_message();
        return false;
    },

    submit: function() {
        show_wait_message();
        jQuery.ajax({
            url: '/empty',
            type: 'post',
            data: this.serialize()
        });
        hide_wait_message();
    },

    disable_input_fields: function(disabled_elements) {
        var disable = function() {
            var elt = this;
            // Disabling hidden elements seems to  make IE sad for some reason
            if(elt.type != 'hidden') {
                // Triggers https://bugzilla.mozilla.org/show_bug.cgi?id=236791
                elt.blur();
                elt.disabled = true;
                disabled_elements.push(elt);
            }
        };
        jQuery.each(this.fields(), disable);
        jQuery.each(this.buttons(), disable);
    },

    enable_input_fields: function() {
        var enable = function() { this.disabled = false; };
        jQuery.each(this.fields(), disable);
        jQuery.each(this.buttons(), disable);
    },


    /* client side logic extracted from Jifty::Action */
    _action_spec: function() {
        if (!this.s_a) {
            /* XXX: make REST client accessible */
            var Todo = new AsynapseRecord('todo');
            this.s_a = jQuery.extend({}, Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
        }

        return this.s_a;
    },
    argument_names: function() {
        return this._action_spec().keys();
    },

    render_param: function(field) {
        var a_s = this._action_spec();
        var type = 'text';
        var f = new ActionField(field, a_s[field], this);
        return f.render();
    },
    register_name: function() { return this.register.id; }

};

var SERIAL_postfix = Math.ceil(10000*Math.random());
var SERIAL = 0;
var ActionField = function() {
    this.initialize.apply(this, arguments);
    return this;
};

ActionField.prototype = {
 initialize: function(name, args, action) {
        this.name = name;
        this.label = args.label;
        this.hints = args.hints;
        this.mandatory = args.mandatory;
        this.ajax_validates = args.ajax_validates;
        this.current_value = action.data_structure().fields[name].value;
        this.error = action.result.field_error[name];
        this.action = action;
        if (!this.render_mode) this.render_mode = 'update';
        this.type = 'text';
    },

 render: function() {
        if (this.render_mode == 'read') {
            return this.render_wrapper(
                        this.render_preamble,
                        this.render_label,
                        this.render_value);
        }
        else {
            return this.render_wrapper(
                        this.render_preamble,
                        this.render_label,
                        this.render_widget,
                        this.render_inline_javascript,
                        this.render_hints,
                        this.render_errors,
                        this.render_warnings,
                        this.render_canonicalization_notes);
        }
    },
 render_wrapper: function () {
        var classes = ['form_field'];
        if (this.mandatory) classes.push('mandatory');
        if (this.name) classes.push('argument-'+this.name);
        var args = arguments;
        var tthis = this;
        return div(function() {
                attr(function(){return ['class', classes.join(' ')]});
                var buf = new Array;
                for (var i = 0; i < args.length; ++i) {
                    buf.push(typeof(args[i]) == 'function' ? args[i].apply(tthis) : args[i]);
                }
                return buf.join('');
            });
    },
    render_preamble: function() {
        var tthis = this;
        return span(function(){attr(function(){return ['class', "preamble"]});
                return tthis.preamble; });
    },

    render_label: function() {
        var tthis = this;
        if(this.render_mode == 'update')
            return label(function(){attr(function(){return['class', "label", 'for', tthis.element_id()];});
                    return tthis.label });
        else
            return span(function(){attr(function(){return['class', "label" ]});
                    return tthis.label });
    },
 input_name: function() {
        return ['J:A:F', this.name, this.action.moniker].join('-');
    },
 render_hints: function() {
        var tthis = this;
        return span(function(){attr(function(){return ['class', "hints"]});
                return tthis.hints });
    },

 render_errors: function() {
        if (!this.action) return '';
        var tthis = this;
        // XXX: post-request handler needs to extract field error messages
        return span(function(){attr(function(){return ['class', "error", 'id', 'errors-'+tthis.input_name()]});
                return tthis.error });
    },

 render_widget: function () {
        var tthis = this;
        return input(function(){
                    attr(function(){
                            var fields = ['type', tthis.type];
                            if (tthis.input_name) fields.push('name', tthis.input_name());
                            fields.push('id', tthis.element_id());
                            if (tthis.current_value) fields.push('value', tthis.current_value);
                            fields.push('class', tthis._widget_class().join(' '));
                            if (tthis.display_length) {
                                fields.push('size', tthis.display_length)
                            }
                            else if (tthis.max_length) {
                                fields.push('size', tthis.max_length)
                            }

                            if (tthis.max_length) fields.push('maxlength', tthis.max_length);
                            if (tthis.disable_autocomplete) fields.push('autocomplete', "off");
                            //" " .$self->other_widget_properties;
                            return fields;
                        })});
    },
 _widget_class: function() {
        var classes = ['form_field'];
        if (this.mandatory)      classes.push('mandatory');
        if (this.name)           classes.push('argument-'+this.name);
        if (this.ajax_validates) classes.push('ajaxvalidation');
        return classes;
    },

 element_id: function() { if(!this._element_id) this._element_id = this.input_name() + '-S' + (++SERIAL + SERIAL_postfix);
                          return this._element_id; },
 __noSuchMethod__: function(name) {
        return '<!-- '+name+' not implemented yet -->';
    }

};

/* Forms */

Jifty.Form = {};

jQuery.extend(Jifty.Form, {
    getElements: function(element) {
        return jQuery(":input", element).get();
    },

    // Return an Array of Actions that are in this form
    getActions: function (element) {
        var elements = [];

        jQuery(":input", element).each(function() {
            if (Jifty.Form.Element.getType(this) == "registration")
                elements.push(Jifty.Form.Element.getAction(this));
        });

        return elements;
    },

    clearPlaceholders: function(element) {
        var elements = Jifty.Form.getElements(element);
        for(var i = 0; i < elements.length; i++) {
            Jifty.Placeholder.clearPlaceholder(elements[i]);
        }
    }
});

var current_actions = {};

Jifty.Form.Element = {};

/* Fields */

jQuery.extend(Jifty.Form.Element, {
    // Get the moniker for this form element
    // Takes an element or an element id
    getMoniker: function (element) {
        element = Jifty.$(element);

        if (/^J:A(:F)+-[^-]+-.+$/.test(element.name)) {
            var bits = element.name.match(/^J:A(?::F)+-[^-]+-(.+)$/);
            return bits[1];
        } else if (/^J:A-(\d+-)?.+$/.test(element.name)) {
            var bits = element.name.match(/^J:A-(?:\d+-)?(.+)$/);
            return bits[1];
        } else {
            return null;
        }
    },

    // Get the Action for this form element
    // Takes an element or an element id
    getAction: function (element) {
        element = Jifty.$(element);
        var moniker = Jifty.Form.Element.getMoniker(element);
        if (!current_actions[moniker])
            current_actions[moniker] = new Action(moniker);
        return current_actions[moniker];
    },

    // Returns the name of the field
    getField: function (element) {
        element = Jifty.$(element);

        if (/^J:A(:F)+-[^-]+-.+$/.test(element.name)) {
            var bits = element.name.match(/^J:A(?::F)+-([^-]+)-.+$/);
            return bits[1];
        } else {
            return null;
        }
    },

    // The type of Jifty form element
    getType: function (element) {
        element = Jifty.$(element);
        if (/^J:A-/.test(element.name)) {
            return "registration";
        } else if (/^J:A:F-/.test(element.name)) {
            return "value";
        } else if (/^J:A:F:F-/.test(element.name)) {
            return "fallback";
        } else {
            return null;
        }
    },

    getValue: function(element) {
        var $el = jQuery(Jifty.$(element));
        if ( $el.is(":checkbox, :radio") ) {
            return $el.is(":checked") ? $el.val() : null;
        }
        return $el.val();
    },

    // Validates the action this form element is part of
    validate: function (element) {
        if ( !jQuery(element).is('.validation_disabled') ) {
            Jifty.Form.Element.getAction(element).validate();
        }
    },

    // Temporarily disable validation
    disableValidation: function(element) {
        jQuery(element).addClass('validation_disabled');
    },

    //Reenable validation
    enableValidation: function(element) {
        jQuery(element).removeClass('validation_disabled');
    },


    // Look up the form that this element is part of -- this is sometimes
    // more complicated than you'd think because the form may not exist
    // anymore, or the element may have been inserted into a new form.
    // Hence, we may need to walk the DOM.
    getForm: function (element) {
        element = Jifty.$(element);

        if (!element)
            return null;

        if (element.virtualform)
            return element.virtualform;

        if (element.form)
            return element.form;

        for (var elt = element.parentNode; elt != null; elt = elt.parentNode) {
            if (elt.nodeName == 'FORM') {
                element.form = elt;
                return elt;
            }
       }
        return null;
    },

    buttonArguments: function(element) {
        element = Jifty.$(element);
        if (!element)
            return {};

        if (((element.nodeName != 'INPUT') || (element.getAttribute("type") != "submit"))
         && ((element.nodeName != 'A')     || (! element.getAttribute("name"))))
            return {};

        if (element.getAttribute("name").length == 0)
            return {};

        var extras = {};

        // Split other arguments out, if we're on a button
        var pairs = element.getAttribute("name").split("|");
        for (var i = 0; i < pairs.length; i++) {
            var bits = pairs[i].split('=',2);
            extras[ bits[0] ] = bits[1];
        }
        return extras;
    },

    buttonActions: function(element) {
        element = Jifty.$(element);
        var actions = Jifty.Form.Element.buttonArguments(element)['J:ACTIONS'];
        if(actions) {
            return actions.split(",");
        } else {
            return new Array();
        }
    },

    buttonFormElements: function(element) {
        element = Jifty.$(element);

        var extras = [];
        if (!element)
            return extras;

        var args = Jifty.Form.Element.buttonArguments(element);

        jQuery.each(args, function(k, v) {
            var e = document.createElement("input");
            e.setAttribute("type", "hidden");
            e.setAttribute("name", k);
            e.setAttribute("value", v);
            e['virtualform'] = Jifty.Form.Element.getForm(element);
            extras.push(e);
        });

        return extras;
    },

    /* Someday Jifty may have the concept of "default"
       buttons.  For now, this clicks the first button that will
       submit the action associated with the form element.
     */
    clickDefaultButton: function(element) {
        var action = Jifty.Form.Element.getAction( element );
        if ( action ) {
            var buttons = action.buttons();
            for ( var i = 0; i < buttons.length; i++ ) {
                var b = buttons[i];
                if ( Jifty.Form.Element.buttonActions( b ).indexOf( action.moniker ) >= 0 ) {
                    b.click();
                    return true;
                }
            }
        }
        return false;
    },

    handleEnter: function(event) {
        /* Trap "Enter" */
        if (    event.keyCode == 13
             && !event.metaKey && !event.altKey && !event.ctrlKey )
        {
            if ( Jifty.Form.Element.clickDefaultButton( event.target ) )
                event.preventDefault();
        }
    }

});

// Form elements should AJAX validate if the CSS says so
Behaviour.register({
    'input.ajaxvalidation, textarea.ajaxvalidation, input.ajaxcanonicalization, textarea.ajaxcanonicalization': function(elt) {
        jQuery(elt).bind('blur', function () {
            Jifty.Form.Element.validate(elt);
        });
    },
    'input.date': function(e) {
        if ( !jQuery(e).hasClass('has_calendar_link') ) {
            Jifty.Utils.createCalendarLink(e);
            jQuery(e).addClass('has_calendar_link');
        }
    },
    'input.time': function(e) {
        jQuery(e).timepickr({handle: jQuery(e)});
    },
    'input.datetime': function(e) {
        if ( !jQuery(e).hasClass('has_datetime_link') ) {
            Jifty.Utils.createDateTimeLink(e);

            var button = document.createElement('input');
            button.setAttribute('type',  'button');
            jQuery(button).insertAfter(e);
            jQuery(button).timepickr({val: 'Pick time'});
            jQuery(button).blur( function() {
                var old_value = jQuery(this).prev().val();
                if ( Jifty.Calendar.dateRegex.test(old_value) ) {
                    var bits = old_value.match(Jifty.Calendar.dateRegex);
                    old_value = bits[1] + '-' + bits[2] + '-' + bits[3];
                }
                var time = jQuery(button).val();
                jQuery(e).val(  old_value + ' ' + time + ':00' );

                // Trigger an onchange event for any listeners
                jQuery(e).change();
                jQuery(button).val('');
            }
            );
            jQuery(e).addClass('has_datetime_link');
        }
    },
    'input.button_as_link': function(e) {
        Jifty.Utils.buttonToLink(e);
    },
    "input.date, input.text": function(e) {
        /* XXX TODO: Figure out how to make our enter handler detect
           when the autocomplete is active so we can use it on autocompleted
           fields
         */
        if (   !jQuery(e).hasClass("jifty_enter_handler_attached" )
               && !jQuery(e).hasClass("ajaxautocompletes" )
               && !jQuery(e).hasClass("disable_enter_handler") )
        {
            /* Do not use keydown as the event, it will not work as expected in Safari */
            jQuery(e).bind('keypress', Jifty.Form.Element.handleEnter).addClass("jifty_enter_handler_attached");
        }
    },
    ".messages": function(e) {
        if ( !jQuery(e).hasClass('jifty_enter_handler_attached') ) {
            jQuery(e).prepend(
                '<a  href="#" id="dismiss_'+e.id+'" title="Dismiss" onmousedown="this.onfocus=this.blur;" onmouseup="this.onfocus=window.clientInformation?null:window.undefined" onclick="Jifty.Effect(this.parentNode, \'Fade\'); return false;">Dismiss</a>'
                ).addClass("jifty_enter_handler_attached" );

        }
    },
    '.form_field .error, .form_field .warning, .form_field .canonicalization_note': function(e) {
        if ( e.innerHTML == "" ) {
            e.style.display = "none";
        }
    },
    // Form elements should focus if the CSS says so.
    ".focus": function(e) {
        /* Check to see if the element is already focused */
        if (!jQuery(e).hasClass("focused")) {
            jQuery(e).addClass("focused").focus();
        }
    },
    // Hide with javascript (since the user may have even partially-working CSS
    // but no JS)
    ".jshide": function(e) {
        jQuery(e).hide();
    },
    /* Use jQuery for full-page-refresh notifications, as well */
    '#messages.jifty.results.messages, #errors.jifty.results.messages, .popup_message, .popup_error': function(e) {
        jQuery(e).hide();
    },
    '#messages.jifty.results.messages .message, .popup_message': function(e) {
        var sticky = jQuery(e).hasClass('popup_sticky');
        jQuery.jGrowl( e.innerHTML, {
            sticky: sticky,
            theme: 'result-message'
        });
    },
    '#errors.jifty.results.messages .error, .popup_error': function(e) {
        var sticky = jQuery(e).hasClass('popup_sticky');
        jQuery.jGrowl( e.innerHTML, {
            sticky: sticky,
            theme: 'result-error'
        });
    }
});


/* Regions */
// Keep track of the fragments on the page
Jifty.fragments = {};

// Todo: This "fragments" variable should be localized. External access should be restricted
// to use "Jifty.fragments" instead.
var fragments = Jifty.fragments;

var Region = function() {
    this.initialize.apply(this, arguments);
    return this;
};

Region.prototype = {
    initialize: function(name, args, path, parent, in_form) {
        this.name = name;
        this.args = jQuery.extend({}, args);
        this.path = path;
        this.parent = parent ? fragments[parent] : null;
        this.in_form = in_form;
        if (fragments[name]) {
            // If this fragment already existed, we want to wipe out
            // whatever evil lies we might have said earlier; do this
            // by clearing out everything that looks relevant
            jQuery.each(current_args, function(k, v) {
                var parsed = k.match(/^(.*?)\.(.*)/);
                if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
                    current_args[k] = null;
                }
            });
        }

        fragments[name] = this;
    },

    setPath: function(supplied) {
        var self = this;

        // Merge in from current_args
        jQuery.each(current_args, function(k, v) {
            if (k == self.name) {
                self.path = v
            }
        });

        // Update with supplied
        if (supplied != null) {
            this.path = supplied;
        }

        // Propagate back to current args
        current_args[ this.name ] = this.path;

        // Return new value
        return this.path;
    },

    setArgs: function(supplied) {
        var self = this;
        // Merge in current args
        jQuery.each(current_args, function(k, v) {
            var parsed = k.match(/^(.*?)\.(.*)/);
            if ((parsed != null) && (parsed.length == 3) && (parsed[1] == this.name)) {
                self.args[ parsed[2] ] = v
            }
        });
        // Merge in any supplied parameters
        jQuery.extend(this.args, supplied);

        // Fill supplied parameters into current args
        jQuery.each(supplied, function(k, v) {
            current_args[ self.name + '.' + k ] = v;
        });

        // Return new values
        return this.args;
    },

    data_structure: function(path, args) {
        // Set the path and args, if given
        if (path)
            this.setPath(path);
        if (args)
            this.setArgs(args);

        // If we have a parent, find our not-qualified name
        var shortname = this.name;
        if (this.parent) {
            shortname = this.name.substr(this.parent.name.length + 1);
        }

        // Return a nummy data structure
        return {
            name: shortname,
            path: this.path,
            args: this.args,
            parent: this.parent ? this.parent.data_structure(null,null) : null
        }
    }
};


// Keep track of the state variables.
var current_args = {};

// Prepare element for use in update()
//  - 'fragment' is a hash, see fragments in update()

function prepare_element_for_update(f) {
        var name = f['region'];

        // Find where we are going to go
        var element = document.getElementById('region-' + f['region']);
        if (f['element']) {
            element = jQuery(f['element'])[0];
        }
        f['element'] = element;

        // If we can't find out where we're going, bail
        if (element == null)
            return;

        // If we're removing the element, do it now
        if (f['mode'] == "Delete") {
            fragments[name] = null;
            if (f['effect']) {
                Jifty.Effect(
                    Jifty.$('region-'+f['region']),
                    f['effect'],
                    f['effect_args']
                );
                jQuery(element).queue(function() {
                    jQuery(element).remove();
                    jQuery(element).dequeue();
                });
            } else if (f['remove_effect']) {
                Jifty.Effect(
                    Jifty.$('region-'+f['region']),
                    f['remove_effect'],
                    f['remove_effect_args']
                );
                jQuery(element).queue(function() {
                    jQuery(element).remove();
                    jQuery(element).dequeue();
                });
            } else {
                jQuery(element).remove();
            }
            return;
        }

        f['is_new'] = (fragments[name] ? false : true);
        // If it's new, we need to create it so we can dump it
        if (f['is_new']) {
            // Find what region we're inside
            f['parent'] = null;
            if (f['mode'] && ((f['mode'] == "Before") || (f['mode'] == "After")))
                element = element.parentNode;
            while ((element != null) && (element.getAttribute) && (f['parent'] == null)) {
                if (/^region-/.test(element.getAttribute("id")))
                    f['parent'] = element.getAttribute("id").replace(/^region-/,"");
                element = element.parentNode;
            }

            if (f['parent']) {
                f['region'] = name = f['parent'] + '-' + name;
            }

            // Make the region (for now)
            new Region(name, f['args'], f['path'], f['parent'], f['parent'] ? fragments[f['parent']].in_form : null);
        } else if ((f['path'] != null) && f['toggle'] && (f['path'] == fragments[name].path)) {
            // If they set the 'toggle' flag, and clicking wouldn't change the path
            jQuery(element).empty();
            fragments[name].path = null;
            return;
        } else if (f['path'] == null) {
            // If they didn't know the path, fill it in now
            f['path'] == fragments[name].path;
        }

    return f;
}

var walk_node = function(node, table) {
    for (var child = node.firstChild;
         child != null;
         child = child.nextSibling) {
        var name = child.nodeName.toLowerCase();
        if (table[name])
            table[name](child);
    }
}

// applying updates from a fragment
//   - fragment: the fragment from the server
//   - f: fragment spec
var apply_fragment_updates = function(fragment, f) {
    // We found the right fragment
    var dom_fragment = fragments[ f['region'] ];
    var new_dom_args = {};

    var element = f['element'];
    walk_node(
        fragment,
        {
            argument: function(fragment_bit) {
                // First, update the fragment's arguments
                // with what the server actually used --
                // this is needed in case there was
                // argument mapping going on
                var textContent = '';
                if (fragment_bit.textContent) {
                    textContent = fragment_bit.textContent;
                } else if (fragment_bit.firstChild) {
                    textContent = fragment_bit.firstChild.nodeValue;
                }
                new_dom_args[ fragment_bit.getAttribute("name") ] = textContent;
            },
            content: function(fragment_bit) {
                var textContent = '';
                if (fragment_bit.textContent) {
                    textContent = fragment_bit.textContent;
                } else if (fragment_bit.firstChild) {
                    textContent = fragment_bit.firstChild.nodeValue;
                }

                // Re-arrange all <script> tags to the end of textContent.
                // This approach easily deal with the uncertain amount of
                // time we need to wait before the region is ready for running
                // some javascript.

                var re = new RegExp('<script[^>]*>([\\S\\s]*?)<\/script>', 'img');
                var scripts = (textContent.match(re) || []).join("");
                var textContentWithoutScript = textContent.replace(re, '');
                textContent = textContentWithoutScript + scripts;

                // Once we find it, do the insertion
                if (f['mode'] == 'Popout') {
                    jQuery.facebox(textContent);
                    element = document.getElementById('region-' + f['region']);
                } else if (f['mode'] && (f['mode'] != 'Replace')) {
                    var method = ({
                        After: 'after',
                        Before: 'before',
                        Bottom: 'append',
                        Top: 'prepend'
                    })[ f['mode'] ];

                    jQuery.fn[method].call(jQuery(element), textContent);
                    element = document.getElementById('region-' + f['region']);
                } else if (f['remove_effect']) {
                    Jifty.Effect(
                        Jifty.$('region-'+f['region']),
                        f['remove_effect'],
                        f['remove_effect_args']
                    );
                    jQuery(element).queue(function() {
                        jQuery(element).html( textContent );
                        jQuery(element).dequeue();
                    });
                } else {
                    jQuery(element).html( textContent );
                }
                Behaviour.apply(element);
            }
        }
    );
    dom_fragment.setArgs(new_dom_args);

    // Also, set us up the effect
    if (f['effect']) {
        Jifty.Effect(
            Jifty.$('region-'+f['region']),
            f['effect'],
            f['effect_args'],
            {
                before: function() {
                    if(f['is_new'])
                        jQuery(this).hide();
                }
            }
        );
    }
}

/* Region update helper 
 *  Jifty.updateRegion( 'region-name' , '/path' , { id: 123123 , msg: 'blah'  } );
 */
Jifty.updateRegion = function( regionName , path , args , mode ) {
    return Jifty.update({
        fragments: [{ region: regionName ,
            args: args, path: path, mode: mode ? mode : 'Replace' }]
    });
};

/* Update a region. It takes two arguments.
  
   Usage:

    Jifty.update({
        fragments: [{
            region: 'region_name',
            args: { id => 123  }
            path: '/path_to_replace',
            mode: 'Replace'
        }]
    });
   
   Description:

   The first argument is a hash of named parameters, including:
    - 'actions' is an array of monikers to submit
    - 'action_arguments' is a hash of action monikers to hashes of arguments which should override any arguments coming from form fields
          the hash keys for 'action_arguments' are the values of the 'actions' array
    - 'continuation' is ??? Please document me
    - 'hide_wait_message' for when you don't want to see it
    - 'preload' this request is preloading regions
    - 'preload_key' the cache key for using preloaded regions
    - 'headers' is a hash of headers to send in this request
    - 'fragments' is an array of hashes, which may have:
       - 'region' is the name of the region to update
       - 'args' is a hash of arguments to override
       - 'path' is the path of the fragment (if this is a new fragment)
       - 'element' is the CSS selector of the element to update, if 'region' isn't supplied
       - 'mode' is one of 'Replace', 'Top', 'Bottom', 'Before', or 'After'
       - 'effect' is the name of an effect
  
    The second argument is the element (usually a submit button) that triggered
    it.
 */


Jifty.update = function () {
    // Let the regular form submit take over if this browser can't do this
    if (!Jifty.hasAjaxTransport) return true;

    // XXX: prevent default behavior in IE
    if(window.event) {
        window.event.returnValue = false;
    }

    // Load the arguments
    var named_args = arguments[0];
    var trigger    = arguments[1];

    // The YAML/JSON data structure that will be sent
    var request = {};

    // Keep track of disabled elements
    var disabled_elements = [];

    // Set request base path
    request.path = '/__jifty/webservices/xml';

    // Grab extra arguments (from a button)
    var button_args = Jifty.Form.Element.buttonFormElements(trigger);

    // Load the form to which this trigger element belongs
    var form = Jifty.Form.Element.getForm(trigger);

    // If the action is null, take all actions
    if (named_args['actions'] == null) {
        named_args['actions'] = {};

        // Add all the actions into the list to submit
        if (form) {
            jQuery.each(Jifty.Form.getActions(form), function(){
                named_args['actions'][this.moniker] = 1;
            });
        }
    }

    // SPA = Special code related to the Single Page Plugin

    // SPA Variable used to tell us to update __page under some conditions
    var optional_fragments;

    // SPA Update __page when the form calls a continuation
    if (form && form['J:CALL']) {
        optional_fragments = [
            prepare_element_for_update({
                'mode':   'Replace',
                'args':   {},
                'region': '__page',
                'path':   null
            })
        ];
    }

    // Build actions request structure
    var has_request = 0;
    var action_count = 0;
    request['actions'] = {};

    // Go through the monikers and actions we know about
    for (var moniker in named_args['actions']) {

        // SPA The extend moniker is special, skip it
        if (moniker == 'extend')
            continue;

        // Remember this action, we will disable it in a minute
        var disable = named_args['actions'][moniker];

        // Find the information related to this action
        var a = new Action(moniker, button_args);

        // Stuff this into the current actions map
        current_actions[moniker] = a;

        // SPA Special case for Redirect, allow optional, implicit __page
        // from the response to be used.
        if (a.actionClass == 'Jifty::Action::Redirect') {
            (function() {
                var fields = a.fields();
                var path = fields[fields.length - 1];
                optional_fragments = [
                    prepare_element_for_update({
                        'mode':   'Replace',
                        'args':   {},
                        'region': '__page',
                        'path':   path
                    })
                ];
            })()
        }

        // Fill these with empty values for the moment
        a.result = {};
        a.result.field_error = {};

        // Do we have an action registration field?
        if (a.register) {

            // Do we need to worry about a file upload field? If so, we cannot
            // ajax this, do a full form submission.
            //
            // TODO Consider some IFRAME magic to fallback upon?
            if (a.hasUpload()) {

                // XXX: restore default behavior in IE (and Opera, Safari)
                if(window.event) {
                    window.event.returnValue = true;
                }

                return true;
            }

            // Disable the action being submitted
            if(disable) {
                a.disable_input_fields(disabled_elements);
            }

            // Build a list of parameters
            var param = a.data_structure();
            var fields = param.fields;
            var override = named_args['action_arguments'][param.moniker] || {};

            // Override the action fields with action_arguments
            for (var argname in override) {
                if (fields[argname]) {
                    fields[argname].value = override[argname];
                }
                else {
                    fields[argname] = { value: override[argname] };
                }
            }

            // Add the parameters to the request we're building
            request['actions'][moniker] = param;
            ++action_count;

            // Remember that we have a request if we're submitting an action
            ++has_request;
        }
    }

    // Get ready to specify the fragment updates we're looking for
    request.fragments = {};

    var hooks = jQuery.map(Jifty.Update.handler_hooks, function(hook) { return hook.init() });

    // Build the fragments request
    for (var i = 0; i < named_args['fragments'].length; i++) {

        // Grab the current fragment
        var f = named_args['fragments'][i];

        // Put together the data structure that will request the fragment
        f = prepare_element_for_update(f);

        // Skip it if we just deleted the fragment
        if (!f) continue;

        var handled = 0;
        for (var j = 0; j < hooks.length; ++j) {
            if (hooks[j].process_fragment(f)) {
                handled = 1;
                break;
            }
        }
        if (handled)
            continue;

        // Build a fragment request from the path and args
        var name = f['region'];
        var fragment_request = fragments[ name ].data_structure(
            f['path'], f['args']
        );

        // Ask for the wrapper if we are making a new region
        if (f['is_new']) {
            fragment_request['wrapper'] = 1;
        }

        // Is the fragment in a form? Prevent <form></form> tags
        if (fragments[name].in_form) {
            fragment_request['in_form'] = 1;
        }

        // Push it onto the request stack
        request.fragments[name] = fragment_request;

        // Remember that we have a request if we're updating a fragment
        ++has_request;
    }

    jQuery.each(hooks, function() { this.process_update() } );

    if (!has_request) {
        return false;
    }

    // And when we get the result back, we'll want to deal with it
    //
    // NOTE: Success here doesn't mean the server liked the request, but that
    // the HTTP communication succeeded. There still might be errors validating
    // fields, with the app connecting to the database, etc.
    var onSuccess = function(responseXML) {
        if (named_args['preload']) {
            // Did we click on a region we were waiting for? If so, pretend
            // we're not preloading at all and treat this as a regular region
            // load.
            if (Jifty.want_preloaded_regions[ named_args['preload_key'] ]) {
                delete Jifty.want_preloaded_regions[ named_args['preload_key'] ];
            }
            // Otherwise, stash this preloaded region away where we can find it
            // for later (possible) reuse.
            else {
                Jifty.preloaded_regions[ named_args['preload_key'] ] = responseXML;
                return;
            }
        }

        // Grab the XML response
        var response = responseXML.documentElement;

        /* var response is an xml , which's content is like:
        <response>
            <fragment id="__page-region-name">
                <argument name="argument1">value1</argument>
                <argument name="argument2">value2</argument>
                <content>
                        ...
                </content>
            </fragment>
            <result class="MyApp::Action::DoPost" moniker="do-post">
                <success>1</success>
                <content>
                    <title>Title</title>
                    <id>123</id>
                </content>
            </result>
        </response>
        */

        // Look through the action results looking for field validation errors
        walk_node(response, {
            result: function(result) {
                var moniker = result.getAttribute("moniker");
                walk_node(result, {
                    field: function(field) {
                        var error = field.getElementsByTagName('error')[0];

                        // Record the validation errors and such with the form
                        if (error) {
                            var text = error.textContent ? error.textContent
                                        : (error.firstChild ? error.firstChild.nodeValue : '');
                            var action = current_actions[moniker];
                            action.result.field_error[field.getAttribute("name")] = text;
                        }
                    }
                });
            }
        });

        // Re-enable all the controls in the actions we previously disabled
        for ( var i = 0; i < disabled_elements.length; i++ ) {
            disabled_elements[i].disabled = false;
        }

        // empty known action.
        // XXX: we should only need to discard actions being submitted

        // SPA We only care about __page sometimes
        var expected_fragments = optional_fragments ? optional_fragments
                               :                      named_args['fragments'];

        // Loop through the response looking for fragments we requested
        for (var response_fragment = response.firstChild;
                response_fragment != null &&
                    response_fragment.nodeName == 'fragment';
                response_fragment = response_fragment.nextSibling) {

            // Get the returned ID attached to the new fragment for validation
            var exp_id = response_fragment.getAttribute("id");

            // Pull out the expected fragment from args matching the response
            var f;
            jQuery.each(expected_fragments, function() {
                if (exp_id == this['region']) {
                    f = this;
                    return false;
                }
            });

            // If we didn't expect it, skip it
            if (!f) {
                continue;
            }


            // Apply the fragment update to the page
            try {
                apply_fragment_updates(response_fragment, f);
            } catch (e) { alert(e) }

            // f
            jQuery.each(Jifty.Update.response_hooks, function(i) {
                    this(response_fragment, f);
            });

            jQuery('.fragment_updates_attached').trigger('fragment_updates',f);
        }


        jQuery.each(hooks, function() { this.process_update() } );


        // Look through the response again
        walk_node(response, {

            // Report all the action results we have
            result: function(result) {
                for (var key = result.firstChild;
                        key != null;
                        key = key.nextSibling) {
                    show_action_result(result.getAttribute("moniker"),key);
                }
            },

            // If we've been told to redirect, do it
            redirect: function(redirect) {
                document.location =  redirect.firstChild.firstChild.nodeValue;
            }
        });

        // Forget the actions, we're oh-fficially done with them
        current_actions = {}
    };

    // When an HTTP communication failure happens, we need to clean up
    var onFailure = function(transport, message) {
        // We failed, but we at least know we're done waiting
        if (!hide_wait) {
            hide_wait_message_now();
        }

        // Cry like a baby
        // Don't irritate the user about preload failures, maybe it'll work
        // if they actually do click through to the region
        if (!named_args['preload']) {
            alert("Unable to connect to server.\n\nTry again in a few minutes.");
        }

        // Record the failed request (XXX for debugging?)
        Jifty.failedRequest = transport;

        // Re-enable the forms, no sense in locking them up
        for ( var i = 0; i < disabled_elements.length; i++ ) {
            disabled_elements[i].disabled = false;
        }
    };

    // Almost ready to submit! Add the region arguments
    request.variables = {};
    jQuery.each(current_args, function(k, v) {
        request.variables['region-'+k] = v;
    });

    // Add in the continuation information
    request.continuation = named_args['continuation'];

    // Update the region state information or add it, if needed
    for (var i = 0; i < document.forms.length; i++) {
        var form = document.forms[i];

        jQuery.each(current_args, function(k, v) {
            if (form['J:V-region-'+k]) {
                form['J:V-region-'+k].value = v;
            } else {
                var hidden = document.createElement('input');
                hidden.setAttribute('type',  'hidden');
                hidden.setAttribute('name',  'J:V-region-'+k);
                hidden.setAttribute('id',    'J:V-region-'+k);
                hidden.setAttribute('value', v);
                form.appendChild(hidden);
            }
        })
    }

    var submitActions = function () {
        // If we have to submit actions, then it gets more complicated
        // We submit a request with the action and block preloading until
        // the action has returned
        if (action_count > 0) {

            // We do not want any region updates. That is for the preloading
            // request.
            delete request.fragments;

            Jifty.preload_action_request();
            jQuery.ajax({
                url:         document.URL,
                type:        'post',
                dataType:    'xml',
                data:        JSON.stringify(request),
                contentType: 'text/x-json',
                error:       onFailure,
                success:     onSuccess,
                complete:    Jifty.preload_action_respond
            });
        }
    };

    // Are we requesting a region we have preloaded? If so, use the response
    // from the cache instead of making a new request. Snappy!
    if (Jifty.preloaded_regions[ named_args['preload_key'] ]) {
        var faux_response = Jifty.preloaded_regions[ named_args['preload_key'] ];
        delete Jifty.preloaded_regions[ named_args['preload_key'] ];

        submitActions();

        onSuccess(faux_response);
        return false;
    }

    // If we're loading a region, then we should just wait for it instead
    // of making a second request and throwing away the preload. If the
    // onSuccess callback sees the want_preloaded_region it will immediately
    // process it.
    if (Jifty.preloading_regions[ named_args['preload_key'] ]) {
        Jifty.want_preloaded_regions[ named_args['preload_key'] ] = 1;
        submitActions();
        return false;
    }

    // Show the "Loading..." message (or equivalent)
    var hide_wait = named_args['hide_wait_message'];
    if (!hide_wait) {
        show_wait_message();
    }

    // Submit ajax request as JSON; expect XML in return
    jQuery.ajax({
        url:         document.URL,
        type:        'post',
        dataType:    'xml',
        data:        JSON.stringify(request),
        contentType: 'text/x-json',
        error:       onFailure,
        success:     onSuccess,

        // Hide the wait message when we're done
        complete: function() {
            // If we want this same region again, don't reuse it from the cache
            delete Jifty.preloading_regions[ named_args['preload_key'] ];

            if (!hide_wait) {
                hide_wait_message();
            }
        },

        beforeSend: function (request) {
            var headers = named_args['headers'];
            for (header in headers) {
                if (headers.hasOwnProperty(header)) {
                    request.setRequestHeader(header, headers[header]);
                }
            }
        }
    });

    // Disable regular browser form submission (we're Ajaxing instead)
    return false;
}

// A cache of preload_key to XMLresponse objects
Jifty.preloaded_regions = {};

// Are we currently preloading a given preload_key?
Jifty.preloading_regions = {};

// Are we submitting an action? If so, delay preloading.
Jifty.preloading_is_queued = 0;

// A cache of preloads to execute once we have a response to the action
// we submitted
Jifty.queued_preloads = [];

// For when we want a preloading region to be processed immediately (e.g. when
// we click a preloaded button)
Jifty.want_preloaded_regions = {};

Jifty.preload = function (args, trigger) {
    // XXX: prevent default behavior in IE
    if(window.event) {
        window.event.returnValue = false;
    }

    // Don't request the same region multiple times
    if (Jifty.preloading_regions[ args['preload_key'] ]) {
        return false;
    }

    // We're waiting for an action. We don't want to preload any more regions
    // until we know that action has been executed.
    if (Jifty.preloading_is_queued) {
        Jifty.preloading_regions[ args['preload_key'] ] = 1;
        Jifty.queued_preloads.push(function () {
            delete Jifty.preloading_regions[ args['preload_key'] ];
            Jifty.preload(args, trigger);
        });
        return false;
    }

    // Preloading is supposed to be silent
    args.hide_wait_message = 1;

    // Tell Jifty.update to delay processing of the response
    args.preload = 1;

    // Preloading should never submit actions, preloaded regions should be
    // relatively pure
    args.actions = [];

    Jifty.update(args, trigger);

    Jifty.preloading_regions[ args['preload_key'] ] = 1;

    return false;
}

Jifty.preload_action_request = function () {
    ++Jifty.preloading_is_queued;
};

Jifty.preload_action_respond = function () {
    if (--Jifty.preloading_is_queued == 0) {
        var preloads = Jifty.queued_preloads;
        Jifty.queued_preloads = [];

        for (var i = 0; i < preloads.length; ++i) {
            preloads[i]();
        }
    }
};

function update ( named_args, trigger ) {
    alert( 'please use Jifty.update instead of update.' );
    return Jifty.update( named_args, trigger );
}

function trace( msg ){
  if( typeof( jsTrace ) != 'undefined' ){
    jsTrace.send( msg );
  }
}


function show_wait_message (){
    jQuery('#jifty-wait-message').fadeIn(500);
}

function hide_wait_message (){
    jQuery('#jifty-wait-message').fadeOut(200);
}

function hide_wait_message_now() {
    jQuery('#jifty-wait-message').hide();
}

function show_action_result() {
    var moniker = arguments[0];
    var result = arguments[1];
    var status = result.nodeName;

    if (status == 'field') {
        // If this is a field, it has kids which are <error> or <message> -- loop through them
        for (var key = result.firstChild;
             key != null;
             key = key.nextSibling) {
            show_action_result(moniker,key);
        }
        return;
    }

    /* This is a workaround for Safari, which does not support textContent */
    var text = result.textContent
                    ? result.textContent
                    : (result.firstChild ? result.firstChild.nodeValue : '');

    if(status != 'message' && status != 'error') return;

    jQuery.jGrowl( text, {
        theme: 'result-'+status
    });
}

Jifty.addAutocompleter = function (id) {
    var field  = Jifty.$(id);

    // if there's a raw :, jQuery interprets the id as a css selector
    var selector = '#' + id.replace(/:/g, '\\\:');

    jQuery(selector).autocomplete('/__jifty/autocomplete.xml', {
        cacheLength: 0, // disable caching
        delay: 100,
        httpMethod:  'POST',
        contentType: 'text/x-json',
        mungeData:   function (data, term) {
            return JSON.stringify(data);
        },
        parse: function (data) {
            var results = [];
            jQuery(data).find("li").each(function () {

                var label, value;
                jQuery(this).find("span").each(function () {
                    var el = jQuery(this);

                    // argh this is awful but hasClass('informal') returns false!?
                    if (el.attr('class') == 'informal') {
                        label = el.text();
                    }
                    else if (el.attr('class') == 'hidden_value') {
                        value = el.text();
                    }
                });

                // the above *should* be just the following but jQuery is kicking
                // my puppies
                // var label = jQuery(this).find(".informal").text();
                // var value = jQuery(this).find(".hidden_value").text();

                if (value === undefined) {
                    label = jQuery(this).text();
                    value = label;
                }

                results.push({
                    data:   [label, value],
                    result: value
                });
            });
            return results;
        },
        extraParams: function () {
            var action = Jifty.Form.Element.getAction(field);
            var actions = {
                autocomplete: {
                    'moniker': 'autocomplete',
                    'class':   'Jifty::Action::Autocomplete',
                    'fields':  {
                        moniker:  action.moniker,
                        argument: Jifty.Form.Element.getField(field)
                    }
                }
            };

            // the action we're autocompleting
            actions[action.moniker] = action.data_structure();
            actions[action.moniker]['active'] = 0;

            return {
                path:    this.url,
                actions: actions
            };
        },
        formatItem: function (data) {
            var label = data[0];
            var value = data[1];

            if (label == value) {
                return Jifty.Utils.encodeEntities(label);
            }
            else {
                return '<div class="ac_label">' + Jifty.Utils.encodeEntities(label) + '</div>' +
                       '<div class="ac_value">' + Jifty.Utils.encodeEntities(value) + '</div>';
            }
        }
    });
};

Jifty.Placeholder = function() {
    this.initialize.apply(this, arguments);
    return this;
};

jQuery.extend(Jifty.Placeholder.prototype, {
  element: null,
  text: null,

  initialize: function(element, text) {
     this.element = Jifty.$(element);
     this.text = text;
     this.element.placeholderText = this.text;

     var self = this;

     jQuery( this.element ).bind("focus", function(event) {
         self.onFocus();
     }).bind("blur", function(event) {
         self.onBlur();
     });

     this.onBlur();

     var form = Jifty.Form.Element.getForm(element);

     if(form && !form.hasPlaceholders) {
         form.hasPlaceholders = true;
         // We can't attach this event via DOM event methods because
         // we need to call form.submit() sometimes and still have a good
         // way to call this event handler
         form.onsubmit = function () { Jifty.Form.clearPlaceholders(form); };
     }
  },

  onBlur: function() {
     /* On browser back/forward, the placeholder text will be remembered
        for the field, so we want to add the class if the value is the same
        as the placeholder text.  This does have the effect of making it
        impossible to submit a field with the same value as the placeholder. */
     if (this.element.value == '' || this.element.value == this.text) {
         jQuery(this.element).addClass('placeholder').val(this.text);
     }
  },

  onFocus: function() {
     Jifty.Placeholder.clearPlaceholder(this.element);
  }

});

jQuery.extend(Jifty.Placeholder, {
   hasPlaceholder: function(elt) {
       return jQuery(elt).hasClass('placeholder');
  },

  clearPlaceholder: function(elt) {
     // If the element's text isn't the same as its placeholder text, then the
     // browser screwed up and didn't clear our placeholder. Opera on Mac with
     // VirtueDesktops does this some times, and I lose data.
     // These are normalized because sometimes one has \r\n and the other has \n
     if (Jifty.Placeholder.hasPlaceholder(elt)) {
        var value = elt.value.replace(/\r/g, '');
        var placeholder = elt.placeholderText.replace(/\r/g, '');
        if (value == placeholder)
            jQuery(elt).removeClass('placeholder').val('');
     }
  }

});


// Define hasOwnProperty for Safari
if( !Object.prototype.hasOwnProperty ) {
    Object.prototype.hasOwnProperty = function( property ) {
        try {
            var prototype = this.constructor.prototype;
            while( prototype ) {
                if( prototype[ property ] == this[ property ] ) {
                    return false;
                }
                prototype = prototype.prototype;
            }
        } catch( e ) {}
        return true;
    }
}

/*
 * Jifty.Effect Usage:
 *
 * Jifty.Effect(element, "Fade", { duration: 2.0 });
 *
 * When called, instantly perform a js effect on give element.
 *
 * The last arg "option" is a hash. Currently it's only used for
 * specifying callbacks. There are two possible callbacks, before
 * and after. You may specify them like this:
 *
 * Jifty.Effect(element, "Fade", { duration: 2.0 }, {
 *     before: function() { ... },
 *     after: function() { ... }
 * });
 *
 * The "before" callback is called right before the effect starts.
 * The "after" callback is called right after it's started, but not
 * necessarily ended.
 *
 * This function is written to make it possible that a Jifty plugin
 * can override default effects with other fancy javascript
 * libraries. By default, it delegates all the real work to
 * jQuery's built-in effect functions.
 *
 */

Jifty.Effect = function(el, name, args, options) {
    // Scriptaculous. TODO: This should be overrided by Jifty::Prototype plugins instead of coded in here.
    if (typeof Effect != 'undefined') {
        try {
            var effect = eval('Effect.' + name);
            var effect_args  = args || {};
            if (effect) {
                (effect)(el, effect_args);
            }
            return effect;
        } catch ( e ) {}
    }

    if (options == null) options = {};
    // jQuery built-ins
    var effect = name == 'Fade' ? 'fadeOut' :
                    name == 'Appear' ? 'fadeIn' :
                    name == 'SlideDown' ? 'slideDown' :
                    name == 'SlideUp' ? 'slideUp' :
                    name;

    if ( jQuery.isFunction( jQuery(el)[ effect ] ) ) {
        if ( jQuery.isFunction(options["before"]) )
            options["before"].call( el );

        ( jQuery(el)[ effect ] )(args);

        if ( jQuery.isFunction(options["after"]) )
            options["after"].call( el );
    }
};

Jifty.closeLightbox = function () {
    jQuery(document).trigger('close.facebox');
};

/*
 * Provide an alias in Global namespace for backward compatibility.
 * Also Jifty.Form will still work even if prototype.js is loaded
 * after jifty.js.
 */

Form = {};

jQuery.extend(Form, {
    // Return an Array of Actions that are in this form
    getActions: function (element) {
        // DEPRECATED: use Jifty.Form.getActions instead
        return Jifty.Form.getActions(element);
    },
    clearPlaceholders: function(element) {
        // DEPRECATED: use Jifty.Form.clearPlaceholders instead
        return Jifty.Form.clearPlaceholders(element);
    },

    Element: {}
});

jQuery.extend(Form.Element, {
    // Get the moniker for this form element
    // Takes an element or an element id
    getMoniker: function (element) {
        // DEPRECATED: use Jifty.Form.Element.getMoniker instead
        return Jifty.Form.Element.getMoniker(element);
    },

    // Get the Action for this form element
    // Takes an element or an element id
    getAction: function (element) {
        // DEPRECATED: use Jifty.Form.Element.getAction instead
        return Jifty.Form.Element.getAction(element);
    },

    // Returns the name of the field
    getField: function (element) {
        // DEPRECATED: use Jifty.Form.Element.getField instead
        return Jifty.Form.Element.getField(element);
    },

    // The type of Jifty form element
    getType: function (element) {
        // DEPRECATED: use Jifty.Form.Element.getType instead
        return Jifty.Form.Element.getType(element);
    },

    // Validates the action this form element is part of
    validate: function (element) {
        // DEPRECATED: use Jifty.Form.Element.validate instead
        return Jifty.Form.Element.validate(element);
    },

    // Temporarily disable validation
            disableValidation: function(element) {
                // DEPRECATED: use Jifty.Form.Element.disableValidation instead
                return Jifty.Form.Element.disableValidation(element);
        },

            //Reenable validation
            enableValidation: function(element) {
                // DEPRECATED: use Jifty.Form.Element.enableValidation instead
                return Jifty.Form.Element.enableValidation(element);
        },


    // Look up the form that this element is part of -- this is sometimes
    // more complicated than you'd think because the form may not exist
    // anymore, or the element may have been inserted into a new form.
    // Hence, we may need to walk the DOM.
    getForm: function (element) {
        // DEPRECATED: use Jifty.Form.Element.getForm instead
        return Jifty.Form.Element.getForm(element);
    },

    buttonArguments: function(element) {
        // DEPRECATED: use Jifty.Form.Element.buttonArguments instead
        return Jifty.Form.Element.buttonArguments(element);
    },

    buttonActions: function(element) {
        // DEPRECATED: use Jifty.Form.Element.buttonActions instead
        return Jifty.Form.Element.buttonActions(element);
    },

    buttonFormElements: function(element) {
        // DEPRECATED: use Jifty.Form.Element.buttonFormElements instead
        return Jifty.Form.Element.buttonFormElements(element);
    },

    /* Someday Jifty may have the concept of "default"
       buttons.  For now, this clicks the first button that will
       submit the action associated with the form element.
     */
    clickDefaultButton: function(element) {
        // DEPRECATED: use Jifty.Form.Element.clickDefaultButton instead
        return Jifty.Form.Element.clickDefaultButton(element);
    },

    handleEnter: function(event) {
        // DEPRECATED: use Jifty.Form.Element.handleEnter instead
        return Jifty.Form.Element.handleEnter(event);
    }

});
