/**
 * A library of functions to assist with the generation and validation of form
 * elements.
 *
 * Depends on the Yahoo! YAHOO.util.Event library, which should have been
 * included with this file.
 *
 * @copyright Copyright 2009 Spenlen Media, Inc.. All rights reserved.
 * @version   $Id: formMagic.js 57 2008-02-28 17:50:35Z walberty $
 */

/* Check for dependencies.
 */
if (! YAHOO.util.Event) {
    alert('YAHOO.util.Event not found.');
}
if (! YAHOO.util.Dom) {
    alert('YAHOO.util.Dom not found.');
}

/* Some new convenience functions.
 */

/**
 * Like document.getElementsByName() except returns only the first element.
 * If no element with that name exists, returns null.
 */
if (! document.getElementByName) {
    document.getElementByName = function(_name) {
        var _elements = document.getElementsByName(_name);
        if (_elements.length == 0) {
            return null;
        } else {
            return _elements[0];
        }
    }
}

/**
 * Internet Explorer does not have the hasAttribute() function! This ugly
 * workaround is now required for all browsers!
 */
function _SM_hasAttribute(_node, _attribute) {
    if (typeof _node.hasAttribute !== 'undefined') {
        return _node.hasAttribute(_attribute);
    }
    _value = _node.getAttribute(_attribute);
    if (_value) {
        return true;
    }
    return false;
}


/* Automatically add the form initialization function to the window's
 * onload event.
 */
YAHOO.util.Event.addListener(window, 'load', SM_Form_InitializeForms);

/**
 * Stores the reference to the element that currently has focus.
 * @var object
 */
elementWithFocus = null;

/**
 * Stores the reference to the submit button which was clicked.
 * @var object
 */
submitButton = null;

/**
 * For manually-initialized forms, the validator rules to be used upon form
 * submission.
 * @var object
 */
SM_FormMagic_ValidatorRules = {};

/**
 * For manually-initialized forms, the JavaScript functions to run upon submit
 * instead of letting the browser do it (as with AJAX submissions).
 * @var object
 */
SM_FormMagic_onSubmitHandlers = {};


/**
 * Automagic behaviors when a form element receives focus:
 * <ul>
 *  <li>The element with the current focus is tracked.
 *  <li>For text and textarea elements, checks to see if the prompt text is
 *        displayed and removes it.
 * </ul>
 *
 * @return boolean Always returns true.
 */
function SM_FormMagic_OnFocus(_yuiEvent) {
    elementWithFocus = this;
    switch (this.type) {
        case 'text':
        case 'password':
        case 'textarea':
            SM_Form_ClearPromptText(this);
//            this.select();    // so the cursor appears in IE **************************************** THERES WEIRDNESS HERE THAT MUST BE FIXED!!!
            break;
    }
    var _autoHintElement = document.getElementById(this.name + '-AutoHint');
    if (_autoHintElement) {
        SM_Form_ShowElement(_autoHintElement);
    }
    if (window.formMagic_onFocusCallback) {
        formMagic_onFocusCallback(this);
    }
    return true;
}

/**
 * Automagic behaviors when a form element loses focus:
 * <ul>
 *  <li>For text and textarea elements, checks to see if the field is blank. If
 *        so and prompt text is available, restores the prompt.
 *  <li>If a field is currently marked as having a validation error, runs the
 *        validation again.
 * </ul>
 *
 * @return boolean Always returns true.
 */
function SM_FormMagic_OnBlur(_yuiEvent) {
    switch (this.type) {
        case 'text':
            switch (SM_Form_Utility_GetSuffix(this.id)) {
                case 'Date':
                    SM_Form_Calendar_CleanUpDateField(this);
            }
            // break intentionally omitted
        case 'password':
        case 'textarea':
            SM_Form_SetPromptText(this);
            break;
    }
    if (_SM_hasAttribute(this, 'formMagic_validationError')) {
        SM_Form_RevalidateFormElement(this);
    }
    elementWithFocus = null;
    var _autoHintElement = document.getElementById(this.name + '-AutoHint');
    if (_autoHintElement) {
        SM_Form_HideElement(_autoHintElement);
    }
    if (window.formMagic_onBlurCallback) {
        formMagic_onBlurCallback(this);
    }
    return true;
}

function SM_FormMagic_GetElementWithFocus()
{
    return elementWithFocus;
    
    // http://msdn2.microsoft.com/en-us/library/ms533065(VS.85).aspx
    // http://www.w3.org/html/wg/html5/#focus
    // http://developer.mozilla.org/en/docs/DOM:document.activeElement
    // alert(typeof document.activeElement);
}

/**
 * Automagic behaviors when a form element's value changes:
 * <ul>
 * </ul>
 *
 * @return boolean Always returns true.
 */
function SM_FormMagic_OnChange(_yuiEvent) {
    switch (this.type) {
        case 'select-one':
            switch (SM_Form_Utility_GetSuffix(this.id)) {
                case 'DateMonth':
                case 'DateDay':
                case 'DateYear':
                    SM_Form_Calendar_UpdateDayOptions(SM_Form_Calendar_GetIdPrefix(this));
            }
            SM_Form_ManageDetailField(this, true);
            break;
    }
    if (_SM_hasAttribute(this, 'formMagic_validationError')) {
        SM_Form_RevalidateFormElement(this);
    }
    if (window.formMagic_onChangeCallback) {
        formMagic_onChangeCallback(this);
    }
    return true;
}

/**
 * Automagic behaviors when a form element is clicked:
 * <ul>
 * </ul>
 *
 * @return boolean Always returns true.
 */
function SM_FormMagic_OnClick(_yuiEvent) {
    switch (this.type) {
        case 'button':
            switch (SM_Form_Utility_GetSuffix(this.id)) {
                case 'DuplicateButton':
                    SM_Form_DuplicateButtonHandler(this); break;
            }
            break;
        
        case 'checkbox':
        case 'radio':
            SM_Form_ManageDetailField(this, true);
            break;
        
        case 'submit':
            submitButton = this;
            break;

    }
    if (_SM_hasAttribute(this, 'formMagic_validationError')) {
        SM_Form_RevalidateFormElement(this);
    }
    if (window.formMagic_onClickCallback) {
        formMagic_onClickCallback(this);
    }
    return true;
}

function SM_Form_DuplicateButtonHandler(_button) {
    /* So that all of the fields for the newly created container are visible,
     * track the vertical change and scroll the window by that amount.
     */
    var _oldY = YAHOO.util.Dom.getY(_button);
    
    SM_Form_DuplicateTemplate(SM_Form_Utility_StripSuffix(_button.id));
    
    var _deltaY = YAHOO.util.Dom.getY(_button) - _oldY;
    window.scrollBy(0, _deltaY);
}

/**
 * Manages showing/enabling and hiding/disabling the accessory detail field and
 * its enclosing container for the form element.
 *
 * @param Element _formElement
 * @param boolean _setFocus If true, and the detail field is a text or textarea,
 *   sets the focus on that element.
 */
function SM_Form_ManageDetailField(_formElement, _setFocus) {
    if (! (_SM_hasAttribute(_formElement, 'formMagic_detailFieldName') &&
           _SM_hasAttribute(_formElement, 'formMagic_detailFieldTriggerValue')) ) {
        return;
    }
    var _detailFieldElement = document.getElementByName(_formElement.getAttribute('formMagic_detailFieldName'));
    if (! _detailFieldElement) {
        return;
    }
    var _triggerValue = _formElement.getAttribute('formMagic_detailFieldTriggerValue');
    var _showDetailField = false;
    _values = SM_Form_GetFormElementValues(_formElement.name);
    for (var _valueIdx = 0; _valueIdx < _values.length; _valueIdx++) {
        if (_values[_valueIdx] == _triggerValue) {
            _showDetailField = true;
            break;
        }
    }
    if (_showDetailField) {
        SM_Form_EnableElement(_detailFieldElement.name, false);
        SM_Form_ShowContainer(_detailFieldElement.id);
        if ((_setFocus) && (_SM_hasAttribute(_formElement, 'formMagic_detailFieldAutoFocus'))) {
            _setFocus = _formElement.getAttribute('formMagic_detailFieldAutoFocus');
        }
        /* Autofocus has an issue on Internet Explorer right now due to events
         * firing for incorrect elements. Disable this feature for IE.
         */
        if ((_setFocus) && (! YAHOO.util.Event.isIE)) {
            /* If the detail form element is a text field or textarea, set the
             * focus so the user can immediately start typing.
             */
            if ((_detailFieldElement.type == 'text') || (_detailFieldElement.type == 'textarea')) {
                _detailFieldElement.focus();
                _detailFieldElement.select();
            }
        }
    } else {
        SM_Form_HideContainer(_detailFieldElement.id);
        SM_Form_DisableElement(_detailFieldElement.name, false);
    }
}

/**
 * Returns the current values of the named form element as an array of strings.
 *
 * For normal text input and other elements, you get a one-element array. For
 * checkboxes and select-multiples, you will receive as many elements as are
 * checked (which may be zero).
 *
 * @param string _formElementName Name (not ID) of form element.
 * @return array
 */
function SM_Form_GetFormElementValues(_formElementName)
{
    _values = new Array();
    _valueCount = 0;
    _elements = document.getElementsByName(_formElementName);
    for (var _elementIdx = 0; _elementIdx < _elements.length; _elementIdx++) {
        switch (_elements[_elementIdx].type) {
            case 'select':
            case 'select-multiple':
                for (var _optionIdx = 0; _optionIdx < _elements[_elementIdx].options.length; _optionIdx++) {
                    if (_elements[_elementIdx].options[_optionIdx].selected) {
                        _values[_valueCount++] = _elements[_elementIdx].options[_optionIdx].value;
                    }
                }
                break;
        
            case 'checkbox':
            case 'radio':
                if (_elements[_elementIdx].checked) {
                    _values[_valueCount++] = _elements[_elementIdx].value;
                }
                break;
            
            case 'text':
            case 'password':
            case 'textarea':
                if ((_SM_hasAttribute(_elements[_elementIdx], 'formMagic_promptText')) &&
                    (_elements[_elementIdx].value == _elements[_elementIdx].getAttribute('formMagic_promptText'))) {
                    _values[_valueCount++] = '';
                } else {
                    _values[_valueCount++] = _elements[_elementIdx].value;
                }
                break;

            default:
                _values[_valueCount++] = _elements[_elementIdx].value;
        }
    }
    return _values;
}


/**
 * If any of the form elements in the form have validation handlers, evaluates
 * them. If there are failures, highlights them, displays an alert, and prevents
 * the form submission. If all pass, allows the form to be submitted.
 *
 * @param Form _form Form being submitted.
 * @return boolean True if the validation passes; false if not.
 */
function SM_FormMagic_validateForm(_form) {
    var _rules = null;
    
    if (_form.id && SM_FormMagic_ValidatorRules[_form.id]) {
        _rules = SM_FormMagic_ValidatorRules[_form.id];

    } else if (window.formMagic_getValidatorRules) {
        _rules = formMagic_getValidatorRules();

    } else {
        SM_Form_ClearAllPromptTexts(_form);
        return true;
    }
    
    if (! submitButton) {
        submitButton = $('submitButton');
    }
    
    if (submitButton) {
        switch (submitButton.value) {
            case 'Cancel':
            case 'cancel':
            case 'Save for Later':
                /* Bypass validation for cancel and save for later actions.
                 */
                SM_Form_ClearAllPromptTexts(_form);
                return true;
        }
    }
    
    var _failureCount = 0;
    var _firstFailure = '';

    for (var _ruleIdx = 0; _ruleIdx < _rules.length; _ruleIdx++) {
        var _validated;
        if (_rules[_ruleIdx][0] == '*ALL*') {
            _validated = SM_Form_ValidateAllFormElements(_form, _rules[_ruleIdx][1]);
        } else {
            _validated = SM_Form_ValidateFormElement(_rules[_ruleIdx][0], _rules[_ruleIdx][1]);
        }
        if (! _validated) {
            if (++_failureCount == 1) {
                _firstFailure = _rules[_ruleIdx][0];
            }
        }
    }

    if (_failureCount == 0) {
        /* Validation was successful. Clear out any prompts that may still be
         * in text fields prior to submission.
         */
        SM_Form_ClearAllPromptTexts(_form);
        return true;
    }
    
    alert('The form could not be submitted. Please correct the errors in the highlighted fields.');
    return false;
}

function SM_Form_RevalidateFormElement(_element) {
    var _rules;
    if (_element.form.id && SM_FormMagic_ValidatorRules[_element.form.id]) {
        _rules = SM_FormMagic_ValidatorRules[_element.form.id];
    } else {
        _rules = formMagic_getValidatorRules();
    }
    for (var _ruleIdx = 0; _ruleIdx < _rules.length; _ruleIdx++) {
        if (_rules[_ruleIdx][0] == _element.name) {
            SM_Form_ValidateFormElement(_rules[_ruleIdx][0], _rules[_ruleIdx][1]);
        }
    }
}

function SM_Form_ValidateAllFormElements(_form, _validators) {
    var _validated = true;
    for (var _i = 0; _i < _form.length; _i++) {
        if ((! SM_Form_ValidateFormElement(_form[_i].name, _validators)) && (_validated)) {
            _validated = false;
        }
    }
    return _validated;
}

function SM_Form_ValidateFormElement(_formElementName, _validators) {
    _formElements = document.getElementsByName(_formElementName);
    if (_formElements.length < 1) {
        return true;
    }

    if ((_SM_hasAttribute(_formElements[0], 'disabled')) ||
        (_formElements[0].type == 'hidden')) {
            return true;
    }

    var _container = SM_Form_GetContainer(_formElementName);
    if (! _container) {
        return true;  // don't fail!
    }
    if (_container.tagName.toLowerCase() == 'tr') {
        _container = _formElements[0].parentNode;
        while ((_container) && (_container.tagName.toLowerCase() != 'td')) {
            _container = _container.parentNode;
        }
    }

    /* Container for the error message. May or may not be present already.
     */
    var _errorText = document.getElementById(_formElementName + '-ErrorText');
    
    var _errorMessages = new Array();
    var _errorCount = 0;
    
    for (var _i = 0; _i < _validators.length; _i++) {
        var _theError = '';
        switch (_validators[_i][0]) {
            case 'required':
                _theError = SM_Form_Validator_Required(_formElementName);
                break;
            case 'requiredIf':
                _theError = SM_Form_Validator_RequiredIf(_formElementName, _validators[_i][1], _validators[_i][2]);
                break;
            case 'requiredWith':
                _theError = SM_Form_Validator_RequiredWith(_formElementName, _validators[_i][1]);
                break;
            case 'dateRange':
                _theError = SM_Form_Validator_DateRange(_formElementName, _validators[_i][1], _validators[_i][2]);
                break;
        }
        if (_theError) {
            _errorMessages[_errorCount++] = _theError;
        }
    }
    
    if (_errorCount < 1) {
        YAHOO.util.Dom.removeClass(_container, 'validationError');
        if ((_errorText) && (YAHOO.util.Dom.hasClass(_errorText, 'validationErrorText'))) {
            YAHOO.util.Dom.removeClass(_errorText, 'validationErrorText');
            while (_errorText.firstChild) {
                _errorText.removeChild(_errorText.firstChild);
            }
        }
        for (var _i = 0; _i < _formElements.length; _i++) {
            _formElements[_i].removeAttribute('formMagic_validationError');
        }
        return true;
        
    } else {
        YAHOO.util.Dom.addClass(_container, 'validationError');
        if (! _errorText) {
            _errorText = document.createElement('SPAN');
            _errorText.id = _formElementName + '-ErrorText';
            _container.insertBefore(_errorText, _container.firstChild);
        }
        while (_errorText.firstChild) {
            _errorText.removeChild(_errorText.firstChild);
        }
        for (var _i = 0; _i < _errorMessages.length; _i++) {
            _errorText.appendChild(document.createTextNode(_errorMessages[_i]));
            _errorText.appendChild(document.createElement('BR'));
        }
        YAHOO.util.Dom.addClass(_errorText, 'validationErrorText');
        for (var _i = 0; _i < _formElements.length; _i++) {
            _formElements[_i].setAttribute('formMagic_validationError', 'true');
        }
        return false;
    }
}


function getLeftEdge(_element) {
    var _leftEdge = 0;
    if (_element.offsetParent) {
        while (_element.offsetParent) {
            _leftEdge += _element.offsetLeft;
            _element = _element.offsetParent;
        }
    } else if (_element.x) {
        _leftEdge = _element.x;
    }
    return _leftEdge;
}

function getTopEdge(_element) {
    var _topEdge = 0;
    if (_element.offsetParent) {
        while (_element.offsetParent) {
            _topEdge += _element.offsetTop;
            _element = _element.offsetParent;
        }
    } else if (_element.y) {
        _topEdge = _element.y;
    }
    return _topEdge;
}

/**
 * Duplicates a template container containing form fields and initializes them
 * to their default values.
 *
 * Duplicates the highest numbered container. Any elements 
 * contained within whose id and/or name uses the same string as a prefix will
 * be updated.
 * @param string _containerElementId Template HTML element ID. May be a container
 *   element such as DIV, TD, or an individual form element like INPUT.
 */
function SM_Form_DuplicateTemplate(_containerPrefix) {
    var _container, _nextContainer;
    var _i = 0;
    while (_nextContainer = document.getElementById(_containerPrefix.concat('-', ++_i))) {
        _container = _nextContainer;
    }
    _i--;
    if (! _container) {
        alert('Cannot find container: ' + _containerPrefix);
        return;
    }
    var _newContainer = _container.cloneNode(true);
SM_Form_RemoveDefaultEvents(_newContainer);  // **********************************************************************************
    var _originalString = _containerPrefix.concat('-', _i);
    var _replaceString  = _containerPrefix.concat('-', ++_i)
    SM_Form_DuplicateTemplate_ProcessElements(_newContainer, _originalString, _replaceString);
    var _insertLocation = document.getElementById(_containerPrefix + '-InsertLocation');
    _insertLocation.parentNode.insertBefore(_newContainer, _insertLocation);
    // make sure the newly created form elements have the default events
    SM_Form_AddDefaultEvents(_newContainer);
    // verify visibility of the detail fields
    SM_Form_DuplicateTemplate_ManageDetailFields(_newContainer);
    // Check if we've reached the maximum number. Disable the add button if so.
    if (window.formMagic_getDuplicateMax) {
        var _max = formMagic_getDuplicateMax(_containerPrefix);
        if ((_max > 0) && (_i >= _max)) {
            var _duplicateButton = document.getElementById(_containerPrefix.concat('-DuplicateButton'));
            if (_duplicateButton) {
                YAHOO.util.Dom.setStyle(_duplicateButton, 'display', 'none');
            }
        }
    }
    if (window.formMagic_duplicateCallback) {
        formMagic_duplicateCallback(_containerPrefix, _i);
    }
}

function SM_Form_DuplicateTemplate_ProcessElements(_element, _originalString, _replaceString) {
    if (_element.id) {
        var _newElementId = _element.id.replace(_originalString, _replaceString);
        _element.id = _newElementId;
        if (_element.name) {
            var _newName = _element.name.replace(_originalString, _replaceString);
            /* Most versions of Safari have a bug which prevents changing the
             * name attribute in-place--it must be removed and replaced.
             */
            if (YAHOO.util.Event.isSafari) {
                _element.removeAttribute('name');
                _element.setAttribute('name', _newName);
            } else {
                _element.name = _newName;
            }
        }
        if (_element.type) {
            switch (_element.type) {
                case 'text':
                case 'password':
                case 'textarea':
                    _element.defaultValue = '';
                    _element.value = '';
                    break;
                case 'select-one':
                case 'select-multiple':
                    _element.selectedIndex = -1;
                    break;
                case 'radio':
                case 'checkbox':
                    _element.defaultChecked = false;
                    _element.checked = false;
                    break;
            }
        }
        SM_Form_SetPromptText(_element);
        if (_SM_hasAttribute(_element, 'formMagic_detailFieldName')) {
            var _newDetailFieldName = _element.getAttribute('formMagic_detailFieldName').replace(_originalString, _replaceString);
            _element.setAttribute('formMagic_detailFieldName', _newDetailFieldName);
        }
    }
    for (var _i = 0; _i < _element.childNodes.length; _i++) {
        SM_Form_DuplicateTemplate_ProcessElements(_element.childNodes[_i], _originalString, _replaceString);
    }
}

function SM_Form_DuplicateTemplate_ManageDetailFields(_element) {
    if ((_element.id) && (_SM_hasAttribute(_element, 'formMagic_detailFieldName'))) {
        switch (_element.type) {
            case 'radio':
            case 'checkbox':
                if (_element.value == _element.getAttribute('formMagic_detailFieldTriggerValue')) {
                    SM_Form_ManageDetailField(_element, false);
                }
                break;
            default:
                SM_Form_ManageDetailField(_element, false);
        }
    }
    for (var _i = 0; _i < _element.childNodes.length; _i++) {
        SM_Form_DuplicateTemplate_ManageDetailFields(_element.childNodes[_i]);
    }
}

/**
 * Disables all form elements with the given name.
 *
 * @param string _formElementName
 * @param boolean _clearCurrentValue
 */
function SM_Form_DisableElement(_formElementName, _clearCurrentValue) {
    _elements = document.getElementsByName(_formElementName);
    for (var _elementIdx = 0; _elementIdx < _elements.length; _elementIdx++) {
        _elements[_elementIdx].setAttribute('disabled', 'disabled');
        if (_clearCurrentValue) {
            switch (_elements[_elementIdx].type) {
                case 'text':
                case 'password':
                case 'textarea':
                    _elements[_elementIdx].value = ''; break;
                case 'checkbox':
                case 'radio':
                    _elements[_elementIdx].checked = false; break;
            }
        }
    }
}

/**
 * Enables all form elements with the given name.
 *
 * @param string _formElementName
 * @param boolean _restoreDefaultValue
 */
function SM_Form_EnableElement(_formElementName, _restoreDefaultValue) {
    _elements = document.getElementsByName(_formElementName);
//alert('looking for ' + _formElementName + '; found: ' + _elements.length);
    for (var _elementIdx = 0; _elementIdx < _elements.length; _elementIdx++) {
//alert('enabling: ' + _elements[_elementIdx].id);
        _elements[_elementIdx].removeAttribute('disabled');
        if (_restoreDefaultValue) {
            switch (_elements[_elementIdx].type) {
                case 'text':
                case 'password':
                case 'textarea':
                case 'hidden':
                    _elements[_elementIdx].value = _elements[_elementIdx].defaultValue;
                    SM_Form_SetPromptText(_elements[_elementIdx]);
                    break;
                case 'checkbox':
                case 'radio':
                    _elements[_elementIdx].checked = _elements[_elementIdx].defaultChecked;
                    break;
            }
        }
    }
}

/**
 * Returns the container for the form element. The container is usually the
 * enclosing block-level element (TD, DIV, P, etc.) but may be identified
 * explicitly by setting the ID of the containing element to
 * "form-Element-Id-Container".
 *
 * @param string _elementId Form element ID
 * @return object Containing element
 */
function SM_Form_GetContainer(_elementId)
{
    var _container = document.getElementById(_elementId + '-Container');
    if (! _container) {
        _currentNode = document.getElementById(_elementId);
        if (! _currentNode) {
            _currentNode = document.getElementByName(_elementId);
            if (! _currentNode) {        
                alert('Cannot find node: ' + _elementId);
                return false;
            }
        }
        do {
            _currentNode = _currentNode.parentNode;
            switch (_currentNode.tagName.toLowerCase()) {
                case 'div':
                case 'p':
                case 'td':
                case 'th':
                case 'dd':
                case 'li':
                case 'body':
                    _container = _currentNode;
            }
        } while (! _container)
    }
    return _container;
}

/**
 * Shows the container element.
 * @param string _idPrefix
 */
function SM_Form_ShowContainer(_elementId) {
    var _container = SM_Form_GetContainer(_elementId);
    if (! _container) {
        return;
    }
    SM_Form_ShowElement(_container);
}
function SM_Form_ShowElement(_element) {
    switch (_element.tagName.toLowerCase()) {
        case 'span':
            YAHOO.util.Dom.setStyle(_element, 'display', 'inline');
            break;
        case 'div':
        case 'p':
        case 'dd':
            YAHOO.util.Dom.setStyle(_element, 'display', 'block');
            break;
        case 'tr':
            /* This should really be set to 'table-row' but Internet Explorer
             * doesn't like that (big surprise). Setting to blank allows the
             * browser's default to be restored.
             */
            YAHOO.util.Dom.setStyle(_element, 'display', '');
            break;
        case 'td':
        case 'th':
            /* Should probably set this to the default 'inherit' instead, but it
             * doesn't work reliably. Set it to blank to restore the browser's
             * default setting.
             */
            YAHOO.util.Dom.setStyle(_element, 'visibility', '');
            break;
    }
}

/**
 * Hides the container element.
 * @param string _elementId
 */
function SM_Form_HideContainer(_elementId) {
    var _container = SM_Form_GetContainer(_elementId);
    if (! _container) {
        return;
    }
    SM_Form_HideElement(_container);
}
function SM_Form_HideElement(_element) {
    switch (_element.tagName.toLowerCase()) {
        case 'span':
        case 'div':
        case 'p':
        case 'tr':
        case 'dd':
            YAHOO.util.Dom.setStyle(_element, 'display', 'none');
            break;
        case 'td':
        case 'th':
            /* Internet Explorer doesn't like 'collapse', so it's 'hidden' for
             * IE, and the more correct 'collapse' for everyone else.
             */
            if (YAHOO.util.Event.isIE) {
                YAHOO.util.Dom.setStyle(_element, 'visibility', 'hidden');
            } else {
                YAHOO.util.Dom.setStyle(_element, 'visibility', 'collapse');
            }
            break;
    }
}

function SM_Form_SetPromptText(_formElement) {
    if (_SM_hasAttribute(_formElement,'formMagic_promptText')) {
        if (_formElement.value == '') {
            YAHOO.util.Dom.addClass(_formElement, 'promptText');
            _formElement.value = _formElement.getAttribute('formMagic_promptText');
        } else if (_formElement.value == _formElement.getAttribute('formMagic_promptText')) {
            YAHOO.util.Dom.addClass(_formElement, 'promptText');
        }
    }
}

function SM_Form_ClearPromptText(_formElement) {
    if ((_SM_hasAttribute(_formElement, 'formMagic_promptText')) &&
        (_formElement.value == _formElement.getAttribute('formMagic_promptText'))) {
        _formElement.value = '';
        YAHOO.util.Dom.removeClass(_formElement, 'promptText');
    }
}

function SM_Form_ClearAllPromptTexts(_form) {
    for (_i = 0; _i < _form.length; _i++) {
        SM_Form_ClearPromptText(_form[_i]);
    }
}

function SM_Form_AddDefaultEvents(_formElement) {
    if ((_formElement.type) && (_formElement.type != 'hidden')) {
        switch (_formElement.type) {
            case 'button':
            case 'checkbox':
            case 'radio':
            case 'submit':
                YAHOO.util.Event.addListener(_formElement, 'click',  SM_FormMagic_OnClick);
                break;
            case 'select-one':
            case 'select-multiple':
                YAHOO.util.Event.addListener(_formElement, 'change',  SM_FormMagic_OnChange);
                break;
        }
        YAHOO.util.Event.addListener(_formElement, 'focus',  SM_FormMagic_OnFocus);
        YAHOO.util.Event.addListener(_formElement, 'blur',   SM_FormMagic_OnBlur);
    }
    for (var _i = 0; _i < _formElement.childNodes.length; _i++) {
        SM_Form_AddDefaultEvents(_formElement.childNodes[_i]);
    }
}

function SM_Form_RemoveDefaultEvents(_formElement) {
    /* This is only required for Internet Explorer as it (still) does not use
     * the W3C DOM event model. When an event is attached to an element, it
     * survives duplication, but there is no way to correct the target. What
     * ends up happening is that events stack on top of one another, and the
     * younger child ends up firing off events for all of its older siblings!
     */
    if (! _formElement.detachEvent) {
        return;
    }
    if ((_formElement.type) && (_formElement.type != 'hidden')) {
        switch (_formElement.type) {
            case 'button':
            case 'checkbox':
            case 'radio':
            case 'submit':
                YAHOO.util.Event.removeListener(_formElement, 'click',  SM_FormMagic_OnClick);
                break;
            case 'select-one':
            case 'select-multiple':
                YAHOO.util.Event.removeListener(_formElement, 'change',  SM_FormMagic_OnChange);
                break;
        }
        YAHOO.util.Event.removeListener(_formElement, 'focus',  SM_FormMagic_OnFocus);
        YAHOO.util.Event.removeListener(_formElement, 'blur',   SM_FormMagic_OnBlur);
    }
    for (var _i = 0; _i < _formElement.childNodes.length; _i++) {
        SM_Form_RemoveDefaultEvents(_formElement.childNodes[_i]);
    }
}

function SM_Form_Utility_GetSuffix(_string) {
    var _index = _string.lastIndexOf('-');
    if (_index < 0) {
        return '';
    }
    return _string.substring(_index + 1);
}

function SM_Form_Utility_StripSuffix(_string) {
    var _index = _string.lastIndexOf('-');
    if (_index < 0) {
        return _string;
    }
    return _string.substring(0, _index);
}

/**
 * Initializes the form elements:
 * <ul>
 *  <li>For text and textarea elements, sets the prompt text if necessary.
 *  <li>Adds the onfocus and onblur handlers.
 * </ul>
 */
function SM_Form_InitializeForms() {
    if (window.formMagic_getPromptStrings) {
        var _promptStrings = formMagic_getPromptStrings();
        SM_Form_InitializePromptStrings(_promptStrings);
    }
    for (var _i = 0; _i < document.forms.length; _i++) {
        SM_Form_AddDefaultEvents(document.forms[_i]);
        
        /* Add automagic handling for 'Other:' values.
         */
        for (var _j = 0; _j < document.forms[_i].elements.length; _j++) {
            switch (document.forms[_i].elements[_j].type) {
                case 'select-one':
                case 'select-multiple':
                    for (var _k = 0; _k < document.forms[_i].elements[_j].options.length; _k++) {
                        if (document.forms[_i].elements[_j].options[_k].value == 'Other:') {
                            var _otherElementName = document.forms[_i].elements[_j].name + '-Other';
                            if (document.getElementByName(_otherElementName)) {
                                document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldTriggerValue', 'Other:');
                                document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldName', _otherElementName);
                                document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldAutoFocus', true);
                                SM_Form_ManageDetailField(document.forms[_i].elements[_j], false);
                            }
                            break;
                        }
                    }
                    break;
                case 'radio':
                    if (document.forms[_i].elements[_j].value == 'Other:') {
                        var _otherElementName = document.forms[_i].elements[_j].name + '-Other';
                        if (document.getElementByName(_otherElementName)) {
                            var _siblingElements = document.getElementsByName(document.forms[_i].elements[_j].name);
                            for (var _k = 0; _k < _siblingElements.length; _k++) {
                                _siblingElements[_k].setAttribute('formMagic_detailFieldTriggerValue', 'Other:');
                                _siblingElements[_k].setAttribute('formMagic_detailFieldName', _otherElementName);
                                _siblingElements[_k].setAttribute('formMagic_detailFieldAutoFocus', true);
                            }
                            SM_Form_ManageDetailField(document.forms[_i].elements[_j], false);
                        }
                    }
                    break;
                case 'checkbox':
                    if (document.forms[_i].elements[_j].value == 'Other:') {
                        var _otherElementName = document.forms[_i].elements[_j].name + '-Other';
                        if (document.getElementByName(_otherElementName)) {
                            document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldTriggerValue', 'Other:');
                            document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldName', _otherElementName);
                            document.forms[_i].elements[_j].setAttribute('formMagic_detailFieldAutoFocus', true);
                            SM_Form_ManageDetailField(document.forms[_i].elements[_j], false);
                        }
                    }
                    break;
            }
            /* If this element has an AutoHint, hide it now.
             */
            _autoHintElement = document.getElementById(document.forms[_i].elements[_j].name + '-AutoHint');
            if (_autoHintElement) {
                SM_Form_HideElement(_autoHintElement);
            }
        }
    }
    
    /* Attributes to manage accessory detail fields.
     */
    if (window.formMagic_getAccessoryFields) {
        var _fields = formMagic_getAccessoryFields();
        for (var _i = 0; _i < _fields.length; _i++) {
            if ((_fields[_i].length == 4) && (document.getElementByName(_fields[_i][2]))) {
                var _formElements = document.getElementsByName(_fields[_i][0]);
                for (var _j = 0; _j < _formElements.length; _j++) {
                    _formElements[_j].setAttribute('formMagic_detailFieldTriggerValue', _fields[_i][1]);
                    _formElements[_j].setAttribute('formMagic_detailFieldName', _fields[_i][2]);
                    _formElements[_j].setAttribute('formMagic_detailFieldAutoFocus', _fields[_i][3]);
                    if (_formElements[_j].value == _fields[_i][1]) {
                        SM_Form_ManageDetailField(_formElements[_j], false);
                    }
                }
            }
        }
    }
    
    /* If there is an autofocus field, focus on it now.
     */
    if (window.formMagic_getAutoFocusField) {
        /* Do our best to not interrupt a user if they've already started to
         * use the form.
         */
        if (! elementWithFocus) {
            var _formElement = document.getElementById(formMagic_getAutoFocusField());
            if (_formElement) {
                _formElement.focus();
            }
        }
    }
}
function SM_Form_InitializePromptStrings(_promptStrings)
{
    if (! _promptStrings) {
        if (window.formMagic_getPromptStrings) {
            _promptStrings = formMagic_getPromptStrings();
        }
    }
    if (! _promptStrings) {
        return;
    }
    for (var _i = 0; _i < _promptStrings.length; _i++) {
        var _element = document.getElementById(_promptStrings[_i][0]);
        if ((_element) && (_element.form) &&
            ((_element.type == 'text') || (_element.type == 'password') || (_element.type == 'textarea'))) {
            _element.setAttribute('formMagic_promptText', _promptStrings[_i][1]);
            SM_Form_SetPromptText(_element);
        }
    }    
}

/**
 * Manually initializes a new form.
 *
 * @param {Form}     theForm
 * @param {Array}    promptStrings   (optional)
 * @param {Array}    validatorRules  (optional)
 * @param {Function} onSubmitHandler (optional)
 */
function SM_FormMagic_InitializeForm(theForm, promptStrings, validatorRules, onSubmitHandler)
{
    if (! theForm.id) {
        return;
    }
    if (promptStrings && promptStrings.length) {
        SM_Form_InitializePromptStrings(promptStrings);
    }
    if (validatorRules && validatorRules.length) {
        SM_FormMagic_ValidatorRules[theForm.id]  = validatorRules;
    }
    if (onSubmitHandler) {
        SM_FormMagic_onSubmitHandlers[theForm.id] = onSubmitHandler;        
    }
    for (var _i = 0; _i < theForm.length; _i++) {
        SM_Form_AddDefaultEvents(theForm[_i]);
    }
    YAHOO.util.Event.addListener(theForm, 'submit',  SM_FormMagic_DoValidateForm);
}

function SM_FormMagic_DoValidateForm(_yuiEvent)
{
    if (SM_FormMagic_validateForm(this)) {
        if (SM_FormMagic_onSubmitHandlers[this.id]) {
            YAHOO.util.Event.stopEvent(_yuiEvent);
            SM_FormMagic_onSubmitHandlers[this.id]();
        }
    } else {
        YAHOO.util.Event.stopEvent(_yuiEvent);
    }
}




/**
 * Returns the form element id prefix used to group related calendar controls.
 *
 * Returns null the form element's id is not one of the four calendar control
 * names.
 *
 * @param Element _formElement A calendar control.
 * @return String
 */
function SM_Form_Calendar_GetIdPrefix(_formElement) {
    var _index = _formElement.id.lastIndexOf('-');
    if (_index < 0) {
        return null;
    }
    switch (_formElement.id.substring(_index + 1)) {
        case 'DateMonth':
        case 'DateDay':
        case 'DateYear':
        case 'DatePicker':
            return _formElement.id.substring(0, _index);
        default:
            return null;
    }
}

/**
 * Returns a JS date object representing the date selected in the form elements. 
 * If there is no value for one of the components, the values from the current
 * date are used.
 *
 * @return Date
 */
function SM_Form_Calendar_GetDate() {
    /* Check for a text entry field.
     */
    _values = SM_Form_GetFormElementValues(theCalendarPrefix);
    if (_values[0]) {
        _theDate = SM_Form_Calendar_ParseDate(_values[0]);
        if (_theDate) {
            return _theDate;
        }
    }
    /* Check for the month/day/year popups.
     */
    _formElement = document.getElementById(theCalendarPrefix + '-DateMonth');
    if (_formElement) {
        var _theDate = new Date();
        var _month = parseInt(_formElement.value);
        if (_month) {
            _theDate.setMonth(_month - 1);
        }
        var _day = parseInt(document.getElementById(theCalendarPrefix + '-DateDay').value);
        if (_day) {
            _theDate.setDate(_day);
        }
        var _year = parseInt(document.getElementById(theCalendarPrefix + '-DateYear').value);
        if (_year) {
            _theDate.setFullYear(_year);
        }
        return _theDate;
    }
    return new Date();
}

/**
 * Returns a JS date object parsed from the MM/DD/YYYY or YYYY-MM-DD formatted
 * string. If the string cannot be parsed or the date contains illegal values,
 * returns null.
 *
 * Accepts "/", "-", ".", and " " as delimiter characters. If the year is two
 * digits, uses 1968/1969 as the pivot year.
 *
 * @param String _dateString
 * @return Date
 */
function SM_Form_Calendar_ParseDate(_dateString) {
    var _year, _month, _day;
    var _pattern = new RegExp("(\\d{1,2})[\\.-/ ](\\d{1,2})[\\.-/ ](\\d{2,4})");
    var _parts = _dateString.match(_pattern);
    if (_parts) {
        _year = parseInt(_parts[3], 10);
        _month = parseInt(_parts[1], 10);
        _day = parseInt(_parts[2], 10);
    } else {
        // Check for ISO 8601 format
        _parts = _dateString.match(/(\d\d\d\d)-(\d?\d)-(\d?\d)/);
        if (_parts) {
            _year = parseInt(_parts[1], 10);
            _month = parseInt(_parts[2], 10);
            _day = parseInt(_parts[3], 10);
        }
    }
    if (_year && _month && _day) {
        if (_year < 1) {
            return null;
        } else if (_year < 69) {
            _year += 2000;
        } else if (_year < 100) {
            _year += 1900;
        }
        if ((_month < 1) || (_month > 12)) {
            return null;
        }
        if ((_day < 1) || (_day > 31)) {
            return null;
        }
        switch (_month) {
            case 2:
                var _isLeapYear;
                if ((_year % 4) != 0) {
                    _isLeapYear = false;
                } else if ((_year % 400) == 0) {
                    _isLeapYear = true;
                } else if ((_year % 100) == 0) {
                    _isLeapYear = false;
                } else {
                    _isLeapYear = true;
                }
                if ((_isLeapYear) && (_day > 29)) {
                    _day = 29;
                } else if (_day > 28) {
                    _day = 28;
                }
                break;
                    
            case 4:
            case 6:
            case 9:
            case 11:
                if (_day > 30) {
                    _day = 30;
                }
                break;
        }
        return new Date(_year, (_month - 1), _day);
    }
    return null;
}

/**
 * Returns a MM/DD/YYYY formatted string from the JS date object.
 *
 * @param Date _theDate
 * @return String
 */
function SM_Form_Calendar_FormatDate(_theDate) {
    return (_theDate.getMonth() + 1) + '/' + _theDate.getDate() + '/' + _theDate.getFullYear();
}

/**
 * Cleans up the date entered into the text field.
 *
 * @param object _dateField
 */
function SM_Form_Calendar_CleanUpDateField(_dateField) {
    _values = SM_Form_GetFormElementValues(_dateField.name);
    if (! _values[0]) {
        return;
    }
    _theDate = SM_Form_Calendar_ParseDate(_values[0]);
    if (_theDate) {
        _dateField.value = SM_Form_Calendar_FormatDate(_theDate);
    }
    SM_Form_RevalidateFormElement(_dateField);
}

/**
 * Ensures that the options in the day popup are appropriate for the selected
 * month/year combination (for example, February on a leap year). Disables
 * invalid options and clamps values that are too high.
 *
 * @param String _prefix Form element id prefix.
 */
function SM_Form_Calendar_UpdateDayOptions(_prefix) {
    var _dayElement = document.getElementById(_prefix + '-DateDay');
    if (! _dayElement) {
        return;
    }
    var _month = parseInt(document.getElementById(_prefix + '-DateMonth').value);
    if (! _month) {
        return;
    }
    switch (_month) {
        case 2:
            var _year = parseInt(document.getElementById(_prefix + '-DateYear').value);
            var _isLeapYear;
            if (! _year) {
                _isLeapYear = true;
            } else if ((_year % 4) != 0) {
                _isLeapYear = false;
            } else if ((_year % 400) == 0) {
                _isLeapYear = true;
            } else if ((_year % 100) == 0) {
                _isLeapYear = false;
            } else {
                _isLeapYear = true;
            }
            if (_isLeapYear) {
                _dayElement.options[29].disabled = '';
                if (parseInt(_dayElement.value) > 29) {
                    _dayElement.value = '29';
                }
            } else {
                _dayElement.options[29].disabled = 'disabled';
                if (parseInt(_dayElement.value) > 28) {
                    _dayElement.value = '28';
                }
            }
            _dayElement.options[30].disabled = 'disabled';
            _dayElement.options[31].disabled = 'disabled';
            break;
            
        case 4:
        case 6:
        case 9:
        case 11:
            _dayElement.options[29].disabled = '';
            _dayElement.options[30].disabled = '';
            _dayElement.options[31].disabled = 'disabled';
            if (parseInt(_dayElement.value) > 30) {
                _dayElement.value = '30';
            }
            break;
        
        default:
            _dayElement.options[29].disabled = '';
            _dayElement.options[30].disabled = '';
            _dayElement.options[31].disabled = '';
            break;
    }
}



/**** The validators ****/

function SM_Form_Validator_Required(_formElementName) {
    _values = SM_Form_GetFormElementValues(_formElementName);
    for (var _valueIdx = 0; _valueIdx < _values.length; _valueIdx++) {
        if (_values[_valueIdx] != '') {
            return '';
        }
    }
    return 'This field is required.';
}

function SM_Form_Validator_RequiredIf(_formElementName, _dependentElementName, _dependentValue) {
    _values = SM_Form_GetFormElementValues(_dependentElementName);
    for (var _valueIdx = 0; _valueIdx < _values.length; _valueIdx++) {
        if (_values[_valueIdx] == _dependentValue) {
            return SM_Form_Validator_Required(_formElementName);
        }
    }
    return '';
}

function SM_Form_Validator_RequiredWith(_formElementName, _dependentElementName) {
    _values = SM_Form_GetFormElementValues(_dependentElementName);
    for (var _valueIdx = 0; _valueIdx < _values.length; _valueIdx++) {
        if (_values[_valueIdx] != '') {
            return SM_Form_Validator_Required(_formElementName);
        }
    }
    return '';
}

/* Doesn't do anything if blank. If this field is required, make sure to add
 * the required validator as well.
 */
function SM_Form_Validator_DateRange(_formElementName, _startDate, _endDate) {
    _values = SM_Form_GetFormElementValues(_formElementName);
    if (_values[0] == '') {
        return '';
    }
    _theDate = SM_Form_Calendar_ParseDate(_values[0]);
    if (_theDate) {
        if ((_theDate >= _startDate) && (_theDate <= _endDate)) {
            return '';
        }
    }
    return 'Must be a date between '
         + SM_Form_Calendar_FormatDate(_startDate) + ' and '
         + SM_Form_Calendar_FormatDate(_endDate);
}
