/** @file
 Functions for form.tpl.
 $Id: form.js 14933 2006-12-04 21:34:29Z tyardley $
 (C) 2005 DHC, Champaign, IL
 */

/** @dir htdocs/script
 Directory for commonly-used JavaScript files (for when header.js gets too
 bloated and needs to spawn a child).
 */

/**
 Called in the body of form.tpl to do all of the validation prep-work for
 the form.
 @param formid DOM id of the form to prepare.
 @param focus optional name for the input field that should gain focus on load.
 @param file_upload boolean on whether or not there are (input type="file")
        fields in the form.
 @param validate array [input name -> validate-object]
 @param passwords array of [input name -> input label] for all passwords
                  that have double-input validation.
 @param buttons array of [input name -> (TBD)] in the form.
 @param onchanges array of [input name -> javascript code block] for
                  onchange events.
 @todo determine exactly what values the buttons array needs
 */
function form_prepare(formid, focus, file_upload, validate, passwords, buttons, onchanges)
{
    var form = document.getElementById(formid);
    if (!form) {
        return;
    }

    // Note: $H has been removed in the move to jquery. This may have side-effects later on,
    // if so, a new hashing function needs to be written.
    form.passwords = (passwords); // array of name => label
    form.validate = (validate); // array of name => validate-info
    form.buttons = (buttons); // array of name => ???
    form.submitcount = 0;

    // add all event handlers
    var name;
    // this is slightly different... hashes drop functions, objects keep them
    for (o in onchanges) {
        if (form[o]) {
            form[o].onchange = onchanges[o];
        }
    }

    if (focus == '1') {
        // default value from template is '1' if not set by the php script
        var elts = form.getElementsByTagName('input');
        var i;
        for (i = 0; i < elts.length; i++) {
            try {
                elts[i].focus();
                break;
            }
            catch (e) {}
        }
    }
    else if (focus && focus.length > 0) {
        // script-specified focus
        var elt = form[focus];
        try {
            elt.focus();
        }
        catch (e) {}
    }

    if (file_upload) {
        // file uploads require this encoding
        form.encoding = "multipart/form-data";
    }
    form.onsubmit = form_validate;
}

/**
 Locate the DOM node to display error messages for an input element
 @param inputnode DOM element of an input node within the form.
 @return the (span class="errornode") element, or null.
 */
function form_errornode(inputnode)
{
    if (!inputnode
     || !inputnode.parentNode
     || !inputnode.parentNode.parentNode
     || !inputnode.parentNode.parentNode) {
        return null;
    }
    var tds = inputnode.parentNode.parentNode.parentNode.getElementsByTagName('td');
    var j;
    for (j = 0; j < tds.length; j++) {
        var spans = tds[j].getElementsByTagName('span');
        var i;
        for (i = 0; i < spans.length; i++) {
            if (spans[i].className == 'formerror') {
                return spans[i];
            }
        }
    }
    return null;
}

/**
 Display an error for the specified input element.
 @param inputnode DOM element of an input node within the form.
 @param error string error message to display for the input node.
 */
function form_error(inputnode, error)
{
    var out = form_errornode(inputnode);
    if (out) {
        out.innerHTML += '<br/>' + error;
    }
}

/**
 Display an error for the overall form (not associated w/ a single input).
 @param form DOM element of the form.
 @param error string error message to display for the form.
 @todo fix this function!
 */
function form_global_error(form, error)
{
    // this can be re-done.
    var out = $('#' + form.id + '_output');
    if (!out) {
        debugAlert('Unable to attach error to form: ' + error);
        return false;
    }
    if (out.firstChild) {
        if (out.firstChild.innerHTML) {
            out.firstChild.innerHTML += '<br/>';
        }
        out.firstChild.innerHTML += error;
    }
    else {
        out.innerHTML += '<span class="formerror">' + error + '</span>';
    }
    return true;
}

/**
 Clear an error for the specified input element.
 @param inputnode DOM element of an input node within the form.
 @see form_error()
 */
function form_clear_error(inputnode)
{
    var out = form_errornode(inputnode);
    if (out) {
        out.innerHTML = '';
    }
}

/**
 Clears a date field (DHTML calendar doesn't offer a control for that).
 @param id the id of the input to clear
 */
function form_clear_date(inputid)
{
    var node;
    node = document.getElementById(inputid);
    if (node) {
        node.value = '';
    }
    node = document.getElementById(inputid + '_display');
    if (node) {
        set_content(node, '');
    }
}


/**
 Returns a new object from the specified regexp, required, and message to
 dictate the validation information for an input.
 @param regexp regular expression object to match.  Omitted if empty.
 @param required boolean of whether the input is required.
 @param message error message to print on validation failure.
                Defaults to "required field".
 @param minval optional minimum integer for the field
 @param maxval optional maximum integer for the field
 @return the newly created object.
 @todo determine finally if minval, maxval should imply required
       (currently they do)
 */
function form_validator(regexp, required, message, minval, maxval)
{
    var v = new Object();
    if (regexp) {
        v.regexp = regexp;
    }
    v.required = required;
    if (minval.length > 0) {
        v.min = parseInt(minval);
    }
    if (maxval.length > 0) {
        v.max = parseInt(maxval);
    }
    if (message.length > 0) {
        v.message = message;
    }
    else if (v.min || v.max) {
        v.required = true;
        if (v.min && v.max) {
            v.message = 'must be between ' + v.min + ' and '
                      + v.max + ' (inclusive)';
        }
        else if (v.min) {
            v.message = 'must be greater than or equal to ' + v.min
        }
        else {
            v.message = 'must be less than or equal to ' + v.max;
        }
    }
    else {
        v.message = 'required field';
    }
    return v;
}

/**
 Perform full validation for passwords, regexp matching, and required fields
 in Javascript.  If any input field fails, displays an error message.
 @note This function is treated as a member function for form objects; "this"
       variable refers to the form.
 @return TRUE if the form validated successfully, else FALSE.
 */
function form_validate() {
    form_toggle_buttons(this, false); // disable submits while we process
    // 'this' is a reference to the form
    var errors = 0;
    var form = this;

    if (! this._novalidate_) {
        // only check if the novalidate field is false.
        $(form.passwords).each(function(i) {
            checker = document.getElementById(this.id + '_verify');
            if (checker && this.value != checker.value) {
                form_error(this, 'passwords do not match');
                errors++;
            }
        });
        $(form.validate).each(function(i) {
            //var input = form.validate[name];
            if (!this || this.value == null) {
                // debug mode.
                //alert(name + ' is not a valid input!');
                return;
            }
            var value = this.value;
            if ((input.regexp && value.length > 0 && !value.match(input.regexp)) ||
                (input.required && value.length < 1) ||
                (input.max && (isNaN(value) || parseInt(value) > input.max)) ||
                (input.min && (isNaN(value) || parseInt(value) < input.min))
                )
            {
                form_error(this, this.value.message);
                errors++;
            }
        });
        if (errors > 0) {
            // we are going to fail.  re-enable submits now.
            form_toggle_buttons(form, true);
        }
    }
    return (errors == 0);
}

/**
 Enable or disable all of the input buttons at the bottom of a form (except
 for the reset button).
 @param form DOM form element
 @param buttons_on boolean of whether the buttons should now be on or off.
 @return FALSE always.
 */
function form_toggle_buttons(form, buttons_on)
{
    var name;

    $(form.buttons).each(function() {
        this.disabled = !buttons_on;
    });
    if (!buttons_on) {
        // turning buttons back on.  allow submits.
        form.submitcount = 0;
        $(form.passwords).each(function() {
            form_clear_error(this);
        });
        $(form.validate).each(function() {
            form_clear_error(this);
        });
    }
    return false;
}

/** "clear" button uses this input */
var RESET_CLEAR = 1;

/** "reload" button uses this input */
var RESET_RELOAD = 2;

/**
 Resets the form and clears any error messages.  Just a fancy wrapper
 for the (input type="reset") button.
 @param formid id of the form element.
 @return FALSE always.
 */
function form_reset(formid, mode)
{
    var form = document.getElementById(formid);
    if (!form) {
        return false;
    }
    switch (mode) {
        case RESET_RELOAD:
            form.reset();
            break;
        case RESET_CLEAR:
            form_clear(form);
            break;
        default:
            // what?  should not be here
            alert('form_reset received invalid mode ' + mode);
            break;
    }
    form.submitcount = 0;
    form_toggle_buttons(form, true);
    var name;

    $(form.validate).each(function(pair) {
        form_clear_error(this);
    });
    return false;
}

/**
 Clears all inputs and all error messages from the specified form.
 @param form HTMLFormElement object
 */
function form_clear(form)
{
    var e, elts;
    var onchange;
    var id, disp;
    elts = form.getElementsByTagName('input');
    for (e = 0; e < elts.length; e++) {
        // skip the buttons, etc.
        if (elts[e].type == 'submit'
         || elts[e].type == 'reset'
         || elts[e].type == 'button'
         || elts[e].type == 'hidden') {
            continue;
        }

        // disable onchange
        onchange = elts[e].onchange;
        elts[e].onchange = null;

        if (elts[e].type == 'checkbox') {
            elts[e].checked = false;
        }
        elts[e].value = '';

        //re-enable any onchange
        elts[e].onchange = onchange;
    }

    elts = form.getElementsByTagName('button');
    for (e = 0; e < elts.length; e++) {
        if (elts[e].className == 'date_clear' && elts[e].onclick) {
            elts[e].onclick();
        }
    }

    elts = form.getElementsByTagName('textarea');
    for (e = 0; e < elts.length; e++) {
        onchange = elts[e].onchange;
        elts[e].onchange = null;

        set_content(elts[e], '');

        elts[e].onchange = onchange;
    }

    elts = form.getElementsByTagName('select');
    for (e = 0; e < elts.length; e++) {
        onchange = elts[e].onchange;
        elts[e].onchange = null;

        elts[e].selectedIndex = 0;

        elts[e].onchange = onchange;
    }
}

/**
 Checks submit count for a form (in case you try to click twice before the
 buttons gray out).  Also declares the _action_ input to whatever the
 specified action value is.  Forwards to the form's onsubmit function.
 @param action new value for the _action_ input field.
 @param novalidate bypasses validation
 @return TRUE if the form validates properly, FALSE otherwise.
 */
function form_presubmit(action, form, novalidate)
{
    form._action_.value = action;
    form._novalidate_ = novalidate;

    submitcount = form.submitcount || 0;
    if (submitcount == 0) {
        form.submitcount = submitcount + 1;
        return form.onsubmit();
    }
    else {
        alert("NOTICE: you already submitted the form");
        return false;
    }
}

/**
 Perform a synchronous form submission (advances the browser to the submitted
 page, and shows up in browser history).
 @param action value to specify for the _action_ input field.  PHP scripts
 should check for this with form_action($_INPUT, $action).
 @param formid id of the form to submit.
 @param novalidate optional boolean to bypass form validation (default false)
 @return FALSE always (as if it matters).
 */
function sync_action(action, formid, novalidate)
{
    novalidate = novalidate || false; // default param
    var form = document.getElementById(formid);
    if (form_presubmit(action, form, novalidate)) {
        form.submit();
    }
    return false;
}

/**
 Perform an asynchronous form submission using http_open() (does not modify
 browser's history, does not reload any page).  Submits via POST.

 This page is mostly useful if a form submits to micro-pages that consist
 only of processing and result-output (the micro-page should *NOT* be
 a complete HTML document).
 @param action action value to specify for the _action_ input field.
 @param formid id of the form to submit.
 @param url URL to POST to.
 @param replaceid id of the DOM element whose innerHTML will be set to the
                  responseText returned by the form.
 @param novalidate boolean to bypass form validation
 @todo decide a default replaceid
 */
function async_action(action, formid, url, replaceid, novalidate)
{
    novalidate = novalidate || false; // default
    var form = document.getElementById(formid);
    var i;
    if (!form_presubmit(action, form, novalidate)) {
        return false;
    }

    open_http('POST', url, form_postdata(form), async_output_cb(form, replaceid));
    return false;
}

/**
 Helper callback for async_action().
 @todo figure out how to make this load Javascript.
 */
function async_output_cb(form, replaceid)
{

    return (function(http) {
        form_toggle_buttons(form, true);
        var output = document.getElementById(replaceid);
        if (output) {
            output.innerHTML = http.responseText;
            var scripts = output.getElementsByTagName('script');
            if (scripts) {
                var src = '';
                var s;
                for (s = 0; s < scripts.length; s++) {
                    if (!scripts[s].src) {
                        //alert(s + ' = ' + get_content(scripts[s]));
                        src += get_content(scripts[s]) + ';';
                    }
                }
                //output.innerHTML += '<pre>' + src + '</pre>';
                //output.innerHTML += eval(src);
            }
        }
    });
}

/**
 Manually create the querystring data that would show up in a GET
 (in the GET /file?querystring line), or a POST (in the HTTP request body).
 Used for asynchronous submissions.
 @param form the form element we wish to submit.
 @return a valid querystring representing the inputs in the form,
         compliant with HTTP 1.1.
 @todo determine if all input types really are RFC-compliant.
 @todo handle multiple-select
 */
function form_postdata(form)
{
    var data = Array();
    var elts;
    elts = form.getElementsByTagName('input');
    for (i = 0; i < elts.length; i++) {
        if (elts[i].name.length > 0) {
            if (elts[i].type == 'checkbox') {
                if (elts[i].checked) {
                    data.push(escape(elts[i].name) + '=on');
                }
            }
            else if (elts[i].type == 'submit' || elts[i].type == 'button') {
                // dont enable the buttons
            }
            else {
                data.push(escape(elts[i].name) + '=' + escape(elts[i].value));
            }
        }
    }

    // get all <select> values
    elts = form.getElementsByTagName('select');
    for (i = 0; i < elts.length; i++) {
        if (elts[i].name.length > 0) {
            data.push(escape(elts[i].name) + '=' + escape(elts[i].value));
        }
    }

    // get all <textarea> values
    elts = form.getElementsByTagName('textarea');
    for (i = 0; i < elts.length; i++) {
        // textContent is Firefox only, I believe
        if (elts[i].name.length > 0) {
            data.push(escape(elts[i].name) + '=' + escape(get_content(elts[i])));
        }
    }
    return data.join('&');
}

/**
 Helper function for phone inputs.  Phone input consists of 3 separate (unnamed)
 input fields, but the collected results are aggregated into a single
 10-character hidden field.
 @param targetid id of the hidden input form.  There should also be three inputs
                 with IDs $id_1, $id_2, $id_3 for the actual text inputs.
 */
function form_phone(targetid)
{
    var i;
    var phone = '';
    var input;
    for (i = 1; i < 4; i++) {
        input = document.getElementById(targetid + '_' + i);
        if (input) {
            phone += input.value;
        }
    }
    input = document.getElementById(targetid);
    if (input) {
        input.value = phone;
    }
}

/**
 Helper function unlocks a form by swapping out "readonly" divs with the
 actual field divs.
 @param formid DOM id of the form
 */
function form_unlock(unlocker, formid)
{
    var form = document.getElementById(formid);
    if (!form) {
        alert('bailing out');
    }
    // some DOM magic to replace the readonly spans
    // with actual input spans.
    var t, tmp;
    var td = form.getElementsByTagName('td');
    var span;
    var classes = '';
    for (t = 0; t < td.length; t++) {
        classes = classes + '\n' + td[t].className;
        if (td[t].className == 'forminput') {
            span = td[t].childNodes;
            if (span.length == 2) {
                // theres one "invisible" and then one "forminput"
                // swap them!
                tmp = span[0].className;
                span[0].className = span[1].className;
                span[1].className = tmp;
            }
        }
    }

    // enable all of our submit buttons
    var b, button;
    button = form.getElementsByTagName('input');
    for (b = 0; b < button.length; b++) {
        if (button[b].type == 'submit') {
            button[b].disabled = false;
        }
    }
    unlocker.disabled = true;
    set_content(unlocker, 'unlocked');
}


