/* ===================================================================================================
* WARNING – This file is part of the base implementation for WebMaker, so it should not be edited or changed for any project.
* These files are replaced if a project is re-imported to the WebMaker Studio or migrated to a new version of the product.
* For guidance on ‘How do I override or clone Hyfinity webapp files such as CSS & javascript?’, please read the following relevant FAQ entry:
* http://www.hyfinity.net/faq/index.php?solution_id=1113
==================================================================================================== */

/**
 * Copyright 2009 Hyfinity Ltd.
 */

if (typeof(hyf) == 'undefined')
{
    hyf = {
        version: 1.0
    }
}


hyf.editabletable = {
    version: 1.1,
    desc: 'Provides functionality for editting/adding/removing entries in a repeating table.',
    EDIT_ID_END: '_et_edit',
    DISPLAY_ID_END: '_et_display',
    HTML_CONTENT_ID_END: '_et_html_content',
    currentRowId: null,  //Stores the ID of the row currently being editted.
    optionsCollection: {},  //links the options collections provided for each table to the table ids
    classes: {
        editRow: 'editabletable_edit_row',
        editBtn: 'editabletable_edit_btn',
        removeBtn: 'editabletable_remove_btn',
        acceptBtn: 'editabletable_accept_btn',
        cancelBtn: 'editabletable_cancel_btn',
        btnContainer: 'editabletable_btn_background',
        insertBtn: 'editabletable_insert_btn',
        insertBtnContainer: 'editabletable_insert_btn_background',
        insertRow: 'editabletable_insert_row'
    }
}

/**
 * Initialises the given tbale, to make it an editable control, with the ability
 * to add, remove and edit entries.
 * @param table The HTML node representing the table to convert
 * @param options (optional) Set of properties that can be used to customize the table:
 *          allow_add : boolean indicating whether the user can add rows to the table (default true)
 *          allow_delete : boolean indicating whether the user can delete rows from the table (default true)
 *          allow_edit : boolean indicating whether the user can edit rows in the table (default true)
 *                      Alternatively, these three properties can contain a reference to a function that will be
 *                      called dynamically to determine the true or false value.  This funciton will be passed the id
 *                      of the relevant table, along with the id of the row where appropriate.
 *          validate : boolean indicating whether the data should be validated before insert/amend (default true)
 *          change_function : function reference (or name) that should be called whenever any change is made
 *                            this function will be passed two parameters, a string indicating the type of
 *                            change - 'insert', 'remove', 'edit', and the id of the row being changed
 */
hyf.editabletable.init = function(table, options)
{
    //alert('creating editable table: ' + table.getAttribute('id'));

    if (typeof(options) != 'undefined')
    {
        if (typeof(options.allow_add) == 'undefined')
            options.allow_add = true;
        if (typeof(options.allow_delete) == 'undefined')
            options.allow_delete = true;
        if (typeof(options.allow_edit) == 'undefined')
            options.allow_edit = true;
        if (typeof(options.validate) == 'undefined')
            options.validate = true;
        if (typeof(options.change_function) == 'undefined')
            options.change_function = null;

        hyf.editabletable.optionsCollection[table.getAttribute('id')] = options;
    }
    else
    {
        hyf.editabletable.optionsCollection[table.getAttribute('id')] = { allow_add: true,
                                                                          allow_delete: true,
                                                                          allow_edit: true,
                                                                          validate: true,
                                                                          change_function: null };
    }

    //we actually want to do the initiation after the document has fully loaded to make
    //sure that any validation attributes etc have been correctly attached
    if (hyf.pageLoaded)
    {
        hyf.editabletable.initImpl(table);
        window.setTimeout('hyf.editabletable.processInitialValuesForTable(document.getElementById("' + table.getAttribute('id') + '"))',100);
    }
    else
        hyf.attachEventHandler(window, 'onload', function() {hyf.editabletable.initImpl(table);});

}

hyf.editabletable.initImpl = function(table)
{
    var options = hyf.editabletable.optionsCollection[table.getAttribute('id')]

    var tbody = document.getElementById(table.getAttribute('id') + '_body');

    //loop through each row to convert to display only and add edit/delete buttons
    var rows = tbody.rows;
    for (var i=0; i < rows.length; ++i)
    {
        if (((rows[i].getAttribute('id') == null) || (rows[i].getAttribute('id') == '')) &&
            (rows[i].cells[0].getAttribute('id') == table.getAttribute('id') + '_empty'))
        {
            tbody.removeChild(rows[i]);
        }
        else
        {
            hyf.editabletable.processRow(rows[i]);
        }
    }

    //add a header th for the new buttons
    var allowEdit = hyf.editabletable.optionsCollection[table.getAttribute('id')].allow_edit;
    var allowDelete = hyf.editabletable.optionsCollection[table.getAttribute('id')].allow_delete;
    if ((allowEdit == true) || (typeof(allowEdit) == 'function') || (allowDelete == true) || (typeof(allowDelete) == 'function'))
    {
        var theadRow = table.tHead.rows[0];
        var newHeadTh = document.createElement('th');
        newHeadTh.colSpan = 2;
        newHeadTh.className = theadRow.cells[0].className;
        newHeadTh.innerHTML = '&nbsp;'
        theadRow.appendChild(newHeadTh);
    }

    //find new row body
    var allowAdd = hyf.editabletable.optionsCollection[table.getAttribute('id')].allow_add;
    if ((allowAdd == true) || ((typeof(allowAdd) == 'function') && allowAdd(table.getAttribute('id'))))
    {
        var newRowBody = document.getElementById(table.getAttribute('id') + '_newRowContent');
        if (newRowBody != null)
        {
            hyf.util.showComponent(newRowBody);

            //take a copy of the initial HTML content of each cell to use when recreating the new blank row after an insert
            for (var i = 0; i < newRowBody.rows[0].cells.length; ++i)
            {
                if (newRowBody.rows[0].cells[i].getAttribute('id') == null)
                    newRowBody.rows[0].cells[i].setAttribute('id', table.getAttribute('id') + 'BlankEntry_cell' + i);

                var newSpan = document.createElement('input');
                newSpan.type = 'hidden';
                newSpan.setAttribute('id', newRowBody.rows[0].cells[i].getAttribute('id') + hyf.editabletable.HTML_CONTENT_ID_END);
                newSpan.value = newRowBody.rows[0].cells[i].innerHTML;
                newRowBody.rows[0].cells[i].appendChild(newSpan);
            }

            var insertTd = document.createElement('td');
            insertTd.setAttribute('id', table.getAttribute('id') + '_insert');
            insertTd.colSpan = 2;
            insertTd.className = newRowBody.rows[0].cells[0].className + ' ' + hyf.editabletable.classes.insertBtnContainer;
            insertTd.innerHTML = '<a class="'+hyf.editabletable.classes.insertBtn+'" href="javascript:hyf.editabletable.insertRow(\''+table.getAttribute('id')+'\')" title="Insert New Entry">Insert</a>';

            newRowBody.rows[0].appendChild(insertTd);
            dojo.addClass(newRowBody.rows[0], hyf.editabletable.classes.insertRow);
        }
    }

    ///special handling for multiple checkbox controls
    dojo.query("input[type=checkbox]", table).forEach(function(item){
        if (item.name != item.id)
            item.name = item.id;
    })

}

/**
 * For the given row in the table, this method hides all the data entry controls,
 * and adds the edit and remove buttons if needed
 */
hyf.editabletable.processRow = function(row)
{
    //loop through all the cells in the row
    var cells = row.cells;
    for (var i = 0; i < cells.length; ++i)
    {
        //move all current contents under a new container
        var editSpan = document.createElement('span');
        editSpan.id = row.id + i + hyf.editabletable.EDIT_ID_END;
        var displaySpan = document.createElement('span');
        displaySpan.id = row.id + i + hyf.editabletable.DISPLAY_ID_END;

        while (cells[i].firstChild != null)
            editSpan.appendChild(cells[i].firstChild);

        cells[i].appendChild(editSpan);
        cells[i].appendChild(displaySpan);

        var value = hyf.editabletable.getCellValue(editSpan);
        if (value == null || "undefined" == typeof(value) || value.length == 0)
            value = " ";

        displaySpan.appendChild(document.createTextNode(value));

        //store the current edit HTML string away for future use
        var editHTML = document.createElement('input');
        editHTML.type = 'hidden';
        editHTML.setAttribute('id', row.id + i + hyf.editabletable.HTML_CONTENT_ID_END);
        editHTML.value = editSpan.innerHTML;
        cells[i].appendChild(editHTML);

        hyf.util.hideComponent(editSpan);

    }

    //add the new edit/remove buttons
    var table = hyf.editabletable.findParentTable(row);

    var allowEdit = hyf.editabletable.optionsCollection[table.getAttribute('id')].allow_edit;
    if ((allowEdit == true) || (typeof(allowEdit) == 'function'))
    {
        var editTd = document.createElement('td');
        editTd.setAttribute('id', row.getAttribute('id') + "_edit");
        row.appendChild(editTd);

        if ((allowEdit == true) || ((typeof(allowEdit) == 'function') && allowEdit(table.getAttribute('id'), row.getAttribute('id'))))
            editTd.innerHTML = '<a class="'+hyf.editabletable.classes.editBtn+'" href="javascript:hyf.editabletable.editRow(\''+row.getAttribute('id')+'\')" title="Edit"><span>Edit</span></a>';
        else
            editTd.innerHTML = '&nbsp;';

        editTd.className = cells[0].className + ' ' + hyf.editabletable.classes.btnContainer;
    }
    var allowDelete = hyf.editabletable.optionsCollection[table.getAttribute('id')].allow_delete;
    if ((allowDelete == true) || (typeof(allowDelete) == 'function'))
    {
        var removeTd = document.createElement('td');
        removeTd.setAttribute('id', row.getAttribute('id') + "_remove");
        row.appendChild(removeTd);

        if ((allowDelete == true) || ((typeof(allowDelete) == 'function') && allowDelete(table.getAttribute('id'), row.getAttribute('id'))))
            removeTd.innerHTML = '<a class="'+hyf.editabletable.classes.removeBtn+'" href="javascript:hyf.editabletable.removeRow(\''+row.getAttribute('id')+'\')" title="Remove"><span>Remove</span></a>';
        else
            removeTd.innerHTML = '&nbsp;';

        removeTd.className = cells[0].className + ' ' + hyf.editabletable.classes.btnContainer;
    }
}

/**
 * Takes the provided HTML componenet and looks through it to find data entry controls,
 * eg textboxes etc.  It then returns a string containing the current value of the control
 * @param container The HTML component to look through for data entry controls
 * @param valueType (optional) A string indicating what type of value should be returned
 *          can be either 'data' or 'display' (the default)
*/
hyf.editabletable.getCellValue = function(container, valueType)
{
    if (typeof(valueType) == 'undefined')
        valueType = 'display';

    //check for a dojo widget instead of a standard control
    if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function') && (dijit.findWidgets(container).length > 0))
    {
        var widget = dijit.findWidgets(container)[0];
        if (valueType == 'display')
        {
            var dv = widget.attr('displayedValue');
            if (dv != null)
                return dv;
        }

        return widget.attr('value');
    }


    var controls = new Array();
    var inputs = container.getElementsByTagName('input');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];
    inputs = container.getElementsByTagName('select');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];
    inputs = container.getElementsByTagName('textarea');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];

    var multiCheckValue = null;

    for (var i = 0; i < controls.length; ++i)
    {
        //look at the type of input
        switch (controls[i].type)
        {
            case 'image'    :   break;
            case 'button'   :   break;
            case 'reset'    :   break;
            case 'submit'   :   break;
            case 'file'     :   break;
            case 'hidden'   :   break;
            case 'password' :   if (valueType == 'data')
                                    return controls[i].value;
                                else
                                {
                                    var stringLength = controls[i].value.length;
                                    var pwdVal = '';
                                    for(var j = 0; j < stringLength; ++j) pwdVal += '*';
                                    return pwdVal;
                                }
            case 'select-one':  if (valueType == 'data')
                                    return controls[i].options[controls[i].selectedIndex].value;
                                else
                                    return controls[i].options[controls[i].selectedIndex].text;
            case 'select-multiple': var returnString = '';
                                    for (var op = 0; op < controls[i].options.length; ++op)
                                    {
                                        if (controls[i].options[op].selected)
                                        {
                                            if (returnString != '')
                                                returnString += ', ';
                                            if (valueType == 'data')
                                                returnString += controls[i].options[op].value;
                                            else
                                                returnString += controls[i].options[op].text;
                                        }
                                    }
                                    return returnString;
            case 'radio'    :   if (controls[i].checked)
                                {
                                    //try and find the matching label
                                    if (valueType == 'display')
                                    {
                                        var labels = container.getElementsByTagName('label');
                                        for (var j = 0; j < labels.length; ++j)
                                        {
                                            var label = labels[j];
                                            if (label.getAttribute('for') == controls[i].id)
                                            {
                                                if (typeof(label.textContent) != 'undefined')
                                                    return label.textContent;
                                                else
                                                    return label.innerText;
                                            }
                                        }
                                    }
                                    //could not find a label so just return control value
                                    return controls[i].value;
                                }
                                //its possible that no radio buttins could yet be selected, so make
                                //sure that we return an empty string in this case, not the text content
                                //of the cell
                                multiCheckValue = '';
                                break;
            case 'checkbox' :   //Check if part of a multi check control
                                if (controls[i].getAttribute('_use') == 'selectMany')
                                {
                                    if (multiCheckValue == null)
                                        multiCheckValue = '';
                                    if (controls[i].checked)
                                    {
                                        if (multiCheckValue != '')
                                            multiCheckValue += ', ';

                                        //try and find the matching label
                                        var labelFound = false;
                                        if (valueType == 'display')
                                        {
                                            var labels = container.getElementsByTagName('label');
                                            for (var j = 0; j < labels.length; ++j)
                                            {
                                                var label = labels[j];
                                                if (label.getAttribute('for') == controls[i].id)
                                                {
                                                    if (typeof(label.textContent) != 'undefined')
                                                        multiCheckValue += label.textContent;
                                                    else
                                                        multiCheckValue += label.innerText;
                                                    labelFound = true;
                                                }
                                            }
                                        }
                                        if (!labelFound)
                                            multiCheckValue += controls[i].value;
                                    }
                                }
                                else
                                {
                                    if (controls[i].checked)
                                        return controls[i].value;
                                    else if (document.getElementById(controls[i].id + '_value_if_not_submitted') != null)
                                        return document.getElementById(controls[i].id + '_value_if_not_submitted').value;
                                    else
                                        return '';
                                }
                                break;
            default         :   return controls[i].value;
        }
    }

    if (multiCheckValue != null)
        return multiCheckValue;

    //if no controls found, then just return the text content of the cell
    if (typeof(container.textContent) != 'undefined')
        return container.textContent;
    else
        return container.innerText;
}

/**
 * Tries to update an editable control in the given container so that it has the provided value.
 * The aim is that for a cell with a single control, calling setCellValue with the output from
 * getCellValue(cell, 'data') above will not affect the HTML.
 */
hyf.editabletable.setCellValue = function(container, valueToSet)
{
    var controls = new Array();
    var inputs = container.getElementsByTagName('input');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];
    inputs = container.getElementsByTagName('select');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];
    inputs = container.getElementsByTagName('textarea');
    for (var i = 0; i < inputs.length; ++i)
        controls[controls.length] = inputs[i];

    //insure that the value is a string
    valueToSet = '' + valueToSet;

    var multiValues = valueToSet.split(', ');


    for (var i = 0; i < controls.length; ++i)
    {
        //look at the type of input
        switch (controls[i].type)
        {
            case 'image'    :   break;
            case 'button'   :   break;
            case 'reset'    :   break;
            case 'submit'   :   break;
            case 'file'     :   break;
            case 'hidden'   :   break;
            case 'select-one':  for(var j = 0; j < controls[i].options.length; ++j)
                                {
                                    if (controls[i].options[j].value == valueToSet)
                                    {
                                        controls[i].options[j].selected = true;
                                        controls[i].options[j].defaultSelected = true;

                                        //make sure the select control no longer has a blank value
                                        //attribute if one has been added
                                        //eg for dojo filtering select to force initial placeholder display
                                        if (controls[i].getAttribute('dojoType') != null)
                                            hyf.util.removeHtmlAttribute(controls[i], 'value');

                                        return;
                                    }
                                }
                                if ((valueToSet == '') && (controls[i].getAttribute('dojoType') != null))
                                    hyf.util.addHtmlAttribute(controls[i], 'value', '');


                                break;
            case 'select-multiple':
                                for (var op = 0; op < controls[i].options.length; ++op)
                                {
                                    var match = false;
                                    for(var j = 0; j < multiValues.length; ++j)
                                    {
                                        if (controls[i].options[op].value == multiValues[j])
                                        {
                                            match = true;
                                        }
                                    }
                                    if (match)
                                    {
                                        controls[i].options[op].selected = true;
                                        controls[i].options[op].defaultSelected = true;
                                    }
                                    else
                                    {
                                        controls[i].options[op].selected = false;
                                        controls[i].options[op].defaultSelected = false;
                                    }
                                }
                                return;
            case 'radio'    :   if (controls[i].value == valueToSet)
                                {
                                    controls[i].checked = true;
                                    controls[i].defaultChecked = true;
                                }
                                else
                                {
                                    controls[i].checked = false;
                                    controls[i].defaultChecked = false;
                                }
                                break;
            case 'checkbox' :   //Check if part of a multi check control
                                if (controls[i].getAttribute('_use') == 'selectMany')
                                {
                                    var val = controls[i].value;

                                    var match = false;
                                    for (var j = 0; j < multiValues.length; ++j)
                                    {
                                        if (val == multiValues[j])
                                        {
                                            match = true;
                                        }
                                    }
                                    if (match)
                                    {
                                        controls[i].checked = true;
                                        controls[i].defaultChecked = true;
                                    }
                                    else
                                    {
                                        controls[i].checked = false;
                                        controls[i].defaultChecked = false;
                                    }
                                }
                                else
                                {
                                    if (controls[i].value == valueToSet)
                                    {
                                        controls[i].checked = true;
                                        controls[i].defaultChecked = true;
                                    }
                                    else
                                    {
                                        controls[i].checked = false;
                                        controls[i].defaultChecked = false;
                                    }
                                    return;
                                }
                                break;
            default         :   controls[i].value = valueToSet;
                                controls[i].defaultValue = valueToSet;
                                return;
        }
    }

}


/**
 * Looks through the parent hierarchy of the given object and returns the first 'table' object forund
 */
hyf.editabletable.findParentTable = function(obj)
{
    return hyf.editabletable.findParentElement(obj, 'table');
}

hyf.editabletable.findParentElement = function(obj, elemName)
{
    if (obj.tagName.toLowerCase() == elemName)
        return obj
    else
        return hyf.editabletable.findParentElement(obj.parentNode, elemName);
}

/**
 * Makes the fields in the given row editable so that the user can change the values.
 * @param rowId The ID of the row to edit.
 */
hyf.editabletable.editRow = function(rowId)
{
    if (hyf.editabletable.currentRowId != null)
        hyf.editabletable.cancelChanges(hyf.editabletable.currentRowId);

    hyf.editabletable.currentRowId = rowId;
    var row = document.getElementById(rowId);

    //hide the display only spans, and make the editable fields visible.
    var spans = row.getElementsByTagName('span');
    for (var i = 0; i < spans.length; ++i)
    {
        var spanId = spans[i].getAttribute('id');
        if ((spanId != null) && (spanId.lastIndexOf(hyf.editabletable.DISPLAY_ID_END) + hyf.editabletable.DISPLAY_ID_END.length == spanId.length))
        {
            hyf.util.hideComponent(spans[i]);
            var editSpan = document.getElementById(spanId.substring(0, spanId.length - hyf.editabletable.DISPLAY_ID_END.length) + hyf.editabletable.EDIT_ID_END);
            hyf.util.showComponent(editSpan);

            ////update the hidden field storing the html content with the latest values
            //so that we can revert ok if they choose to cancel any changes made.
            var contentHidden = document.getElementById(spanId.substring(0, spanId.length - hyf.editabletable.DISPLAY_ID_END.length) + hyf.editabletable.HTML_CONTENT_ID_END);

            var hasWidget = false;
            //if we have dojo widgets, we first need to discard these and manually update the current cell html
            //to make sure we have the updated version
            if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function') && (dijit.findWidgets(editSpan).length > 0))
            {
                var currentValue = hyf.editabletable.getCellValue(editSpan, 'data');
                hyf.util.destroyDojoWidgets(editSpan);
                editSpan.innerHTML = contentHidden.value;
                hyf.editabletable.setCellValue(editSpan, currentValue);
                hasWidget = true;
            }

            //handle the fact that radio and checkbox controls store there defaultchecked property in the innerHTMl,
            //not the current checked property, which is what we actually want.
            var inputsToCheck = editSpan.getElementsByTagName('input');
            for (var j = 0; j < inputsToCheck.length; ++j)
            {
                if ((inputsToCheck[j].type == 'radio') || (inputsToCheck[j].type == 'checkbox'))
                    inputsToCheck[j].defaultChecked = inputsToCheck[j].checked;
                if (typeof(inputsToCheck[j].defaultValue) != 'undefined')
                    inputsToCheck[j].defaultValue = inputsToCheck[j].value;
            }
            contentHidden.value = editSpan.innerHTML;

            //if we have a wdiget, then need to recreate
            if (hasWidget)
            {
                dojo.parser.parse(editSpan);
                hyf.validation.initiateWidgets(editSpan);
            }
        }
    }

    //change the edit/remove buttons to accept/cancel
    var editTd = document.getElementById(rowId + '_edit');
    editTd.innerHTML = '<a class="'+hyf.editabletable.classes.acceptBtn+'" href="javascript:hyf.editabletable.acceptChanges(\''+rowId+'\')" title="Accept Changes"><span>Accept</span></a>';

    var removeTd = document.getElementById(rowId + '_remove');
    if (removeTd != null)
    {
        removeTd.innerHTML = '<a class="'+hyf.editabletable.classes.cancelBtn+'" href="javascript:hyf.editabletable.cancelChanges(\''+rowId+'\')" title="Discard Changes"><span>Cancel</span></a>';
    }
    else
    {
        //delete must be disabled, so need to add temp cancel button
        editTd.innerHTML += '<a class="'+hyf.editabletable.classes.cancelBtn+'" href="javascript:hyf.editabletable.cancelChanges(\''+rowId+'\')" title="Discard Changes"><span>Cancel</span></a>';
    }

    dojo.addClass(row, hyf.editabletable.classes.editRow);
}

/**
 * Stores the changes made to the editable row
 * This updates the displayed spans to contain the new values, and hides the edit boxes.
 * The buttons are also converted back to edit/remove
 * @param rowId The ID of the row being edited.
 */
hyf.editabletable.acceptChanges = function(rowId)
{
    //check if we need to validate first
    var row = document.getElementById(rowId);
    var table = hyf.editabletable.findParentTable(row);
    if (hyf.editabletable.optionsCollection[table.getAttribute('id')].validate)
    {
        if (hyf.validation.validateContainer(row))
        {
            hyf.editabletable.processChanges(rowId, 'accept');
            hyf.editabletable.callChangeFunction(rowId, 'edit');
        }
    }
    else
    {
        hyf.editabletable.processChanges(rowId, 'accept');
        hyf.editabletable.callChangeFunction(rowId, 'edit');
    }
}

/**
 * Discards the changes made to the editable row
 * This reverts the edit boxes back to their old values, and hides them away
 * The buttons are also converted back to edit/remove
 * @param rowId The ID of the row being edited.
 */
hyf.editabletable.cancelChanges = function(rowId)
{
    hyf.editabletable.processChanges(rowId, 'cancel');
}

/**
 * Function that implments the accept/cancel functionality
 * @param rowId The ID of the row being edited.
 * @param type either 'accept' or 'cancel'.
 * @private
 */
hyf.editabletable.processChanges = function(rowId, type)
{
    var row = document.getElementById(rowId);

    //show the display only spans (and update their values), and make the editable fields hidden.
    var spans = row.getElementsByTagName('span');
    for (var i = 0; i < spans.length; ++i)
    {
        var spanId = spans[i].getAttribute('id');
        if ((spanId != null) && (spanId.lastIndexOf(hyf.editabletable.DISPLAY_ID_END) + hyf.editabletable.DISPLAY_ID_END.length == spanId.length))
        {
            var editSpan = document.getElementById(spanId.substring(0, spanId.length - hyf.editabletable.DISPLAY_ID_END.length) + hyf.editabletable.EDIT_ID_END);
            var contentHidden = document.getElementById(spanId.substring(0, spanId.length - hyf.editabletable.DISPLAY_ID_END.length) + hyf.editabletable.HTML_CONTENT_ID_END);
            if (type == 'cancel')
            {
                //check for dojo widgets
                var hasWidget = false;
                if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function') && (dijit.findWidgets(editSpan).length > 0))
                {
                    hyf.util.destroyDojoWidgets(editSpan);
                    hasWidget = true;
                }
                editSpan.innerHTML = contentHidden.value;
                if (hasWidget)
                {
                    dojo.parser.parse(editSpan);
                    hyf.validation.initiateWidgets(editSpan);
                }
            }
            else
            {
                var value = hyf.editabletable.getCellValue(editSpan);
                if (value == null || "undefined" == typeof(value) || value.length == 0)
                    value = "&nbsp;";
                spans[i].innerHTML = value;
            }

            hyf.util.showComponent(spans[i]);
            hyf.util.hideComponent(editSpan);
        }
    }

    //change the accept/cancel buttons back to edit/remove
    var editTd = document.getElementById(rowId + '_edit');
    editTd.innerHTML = '<a class="'+hyf.editabletable.classes.editBtn+'" href="javascript:hyf.editabletable.editRow(\''+row.getAttribute('id')+'\')" title="Edit"><span>Edit</span></a>';

    var removeTd = document.getElementById(rowId + '_remove');
    if (removeTd != null)
    {
        removeTd.innerHTML = '<a class="'+hyf.editabletable.classes.removeBtn+'" href="javascript:hyf.editabletable.removeRow(\''+row.getAttribute('id')+'\')" title="Remove"><span>Remove</span></a>';
    }

    hyf.editabletable.currentRowId = null;
    dojo.removeClass(row, hyf.editabletable.classes.editRow);
}

hyf.editabletable.retrieveNumberFromId = function(id, prefix)
{
    if (id.indexOf(prefix) == 0)
    {
        var num = '';
        var remainder = id.substring(prefix.length);
        num = parseInt(remainder);
        return num;
    }
    return null;
}

/**
 * Removes the specified ID from the table.
 * @param rowId The ID of the row to remove.
 */
hyf.editabletable.removeRow = function(rowId)
{
    if (hyf.editabletable.currentRowId != null)
        hyf.editabletable.cancelChanges(hyf.editabletable.currentRowId);

    hyf.editabletable.callChangeFunction(rowId, 'remove');

    var row = document.getElementById(rowId);

    //update repeat count field
    var table = hyf.editabletable.findParentTable(row)
    hyf.editabletable.manageCount(table, 'remove');

    var rowToAdjust = hyf.util.getNextElementSibling(row);

    //remove row from HTML
    hyf.util.destroyDojoWidgets(row);
    row.parentNode.removeChild(row);

    while (rowToAdjust != null)
    {
        //change IDs of all following rows to reduce repeat number part to prevent future conflicts
        var currentNum = hyf.editabletable.retrieveNumberFromId(rowToAdjust.getAttribute('id'), table.getAttribute('id'));
        var currentId = table.getAttribute('id') + currentNum;
        var newId = table.getAttribute('id') + (parseInt(currentNum) - 1);

        var hasWidget = false;
        //check for any dojo widgets in the row
        if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function') && (dijit.findWidgets(rowToAdjust).length > 0))
        {
            //loop through each cell to check which contains a widget
            for (var i = 0; i < rowToAdjust.cells.length; ++i)
            {
                var cellToAdjust = rowToAdjust.cells[i];

                //check for any dojo widgets in the cell
                if (dijit.findWidgets(cellToAdjust).length > 0)
                {
                    //revert widget to HTML
                    var currentValue = hyf.editabletable.getCellValue(cellToAdjust, 'data');
                    //Destroy any dojo widgets in the row being inserted before processing
                    hyf.util.destroyDojoWidgets(cellToAdjust);

                    //find the edit span and adjust the HTML
                    var editSpan = document.getElementById(rowToAdjust.id + i + hyf.editabletable.EDIT_ID_END);
                    var editHTML = document.getElementById(rowToAdjust.id + i + hyf.editabletable.HTML_CONTENT_ID_END);
                    editSpan.innerHTML = editHTML.value;

                    //update value in the new HTML
                    hyf.editabletable.setCellValue(cellToAdjust, currentValue);
                    hasWidget = true;
                }
            }
        }

        hyf.editabletable.changeIds(rowToAdjust, currentId, newId);

        //change all following row binding xpaths to reduce position number
        var spans = rowToAdjust.getElementsByTagName('span');
        for (var i = 0; i < spans.length; ++i)
        {
            var spanId = spans[i].getAttribute('id');
            if ((spanId != null) && (spanId.lastIndexOf(hyf.editabletable.DISPLAY_ID_END) + hyf.editabletable.DISPLAY_ID_END.length == spanId.length))
            {
                var fieldId = spanId.substring(0, spanId.length - hyf.editabletable.DISPLAY_ID_END.length);
                var editSpan = document.getElementById(fieldId + hyf.editabletable.EDIT_ID_END);
                hyf.editabletable.processXPath(table, editSpan);

                //update the hidden field storing the edit HTML so that it will include the new xpath values
                var contentHidden = document.getElementById(fieldId + hyf.editabletable.HTML_CONTENT_ID_END);
                contentHidden.value = editSpan.innerHTML;
            }
        }

        //reparse any dojo widgets if needed
        if (hasWidget)
        {
            dojo.parser.parse(rowToAdjust);
            hyf.validation.initiateWidgets(rowToAdjust);
        }

        rowToAdjust = hyf.util.getNextElementSibling(rowToAdjust);
    }

}

/**
 * Changes the name/ids etc of every HTML component in the given element
 * so that references to oldId in the strings are replaced with newId.
 */
hyf.editabletable.changeIds = function(elem, oldId, newId)
{
    if (elem.nodeType == 1)
    {
        //QUESTION: Should we just check all attributes?
        if (elem.getAttribute('id') != null)
            elem.setAttribute('id', hyf.editabletable.replaceString(elem.getAttribute('id'), oldId, newId));
        if (elem.getAttribute('name') != null)
        {
            if ((dojo.isIE < 8) && ((elem.tagName.toLowerCase() == 'input') || (elem.tagName.toLowerCase() == 'a') || (elem.tagName.toLowerCase() == 'select') || (elem.tagName.toLowerCase() == 'textarea')))
            {
                //IE versions before 8 do not support dynamicaly changing the name attribute properly
                var newHTML = hyf.editabletable.replaceString(elem.outerHTML, oldId, newId);
                var tempDiv = document.createElement('div');
                tempDiv.innerHTML = newHTML;
                newElem = tempDiv.firstChild;
                elem.parentNode.replaceChild(newElem, elem);
                elem = newElem;
            }
            else
                elem.setAttribute('name', hyf.editabletable.replaceString(elem.getAttribute('name'), oldId, newId));
        }
        if (elem.getAttribute('href') != null)
            elem.setAttribute('href', hyf.editabletable.replaceString(elem.getAttribute('href'), oldId, newId));
        if (elem.getAttribute('onclick') != null)
        {
            if (typeof(elem.getAttribute('onclick')) == 'function')
            {
                var funcString = '' + elem.getAttribute('onclick')
                var converted = hyf.editabletable.replaceString(funcString, oldId, newId);
                convString = converted.substring(converted.indexOf('{') +1, converted.lastIndexOf('}'));
                elem.onclick = Function(convString);
            }
            else
                elem.setAttribute('onclick', hyf.editabletable.replaceString(elem.getAttribute('onclick'), oldId, newId));
        }
        if (elem.getAttribute('onkeypress') != null)
        {
            if (typeof(elem.getAttribute('onkeypress')) == 'function')
            {
                var funcString = '' + elem.getAttribute('onkeypress')
                var converted = hyf.editabletable.replaceString(funcString, oldId, newId);
                convString = converted.substring(converted.indexOf('{') +1, converted.lastIndexOf('}'));
                elem.onkeypress = Function(convString);
            }
            else
                elem.setAttribute('onkeypress', hyf.editabletable.replaceString(elem.getAttribute('onkeypress'), oldId, newId));
        }
        if (elem.getAttribute('for') != null)
            elem.setAttribute('for', hyf.editabletable.replaceString(elem.getAttribute('for'), oldId, newId));
        if (elem.getAttribute('htmlFor') != null)
            elem.setAttribute('htmlFor', hyf.editabletable.replaceString(elem.getAttribute('htmlFor'), oldId, newId));
        if (elem.getAttribute('_element') != null)
            elem.setAttribute('_element', hyf.editabletable.replaceString(elem.getAttribute('_element'), oldId, newId));

        //if this is a hidden field storing the html content, then we need to update this html string value
        //to contain the new id where needed
        if ((elem.tagName.toLowerCase() == 'input') && (elem.type == 'hidden') &&
            (elem.id.lastIndexOf(hyf.editabletable.HTML_CONTENT_ID_END) + hyf.editabletable.HTML_CONTENT_ID_END.length == elem.id.length))
        {
            elem.value = hyf.editabletable.replaceString(elem.value, oldId, newId);
        }

        for (var i = 0 ;i < elem.childNodes.length; ++i)
        {
            hyf.editabletable.changeIds(elem.childNodes.item(i), oldId, newId);
        }
    }
}

hyf.editabletable.processXPath = function(table, container, predicateValue)
{
    var inputs = container.getElementsByTagName('input');
    for (var i = 0; i < inputs.length; ++i)
    {
        if (inputs[i].type == 'hidden')
        {
            /*if ((inputs[i].name == (fieldID + '_xpath')) ||
                (inputs[i].name.indexOf('xgf_' + fieldID + '_xpath') == 0))*/
            if (inputs[i].name.lastIndexOf('_xpath') == inputs[i].name.length - 6)
            {
                inputs[i].value = hyf.editabletable.processXPathString(table, inputs[i].value, predicateValue, null);
            }
            else if ((inputs[i].name.lastIndexOf('_xpath') == inputs[i].name.length - 6) ||
                (inputs[i].name.indexOf('xgf_') == 0 && inputs[i].name.indexOf('_xpath_') != -1))
            {
                inputs[i].value = hyf.editabletable.processXPathString(table, inputs[i].value, predicateValue, inputs[i].name.substr(inputs[i].name.lastIndexOf('_xpath_') + 6));
            }
        }
    }
}

hyf.editabletable.processXPathString = function(table, xpathString, predicateValue, action)
{
    //find the context xpath for the repeat
    var contextXPath;
    var possibleContexts = table.parentNode.getElementsByTagName('input');
    for (var i = 0; i< possibleContexts.length; ++i)
    {
        if ((possibleContexts[i].type == 'hidden') &&
            (possibleContexts[i].name.indexOf('hyfrepeat-') == 0) &&
            (possibleContexts[i].name.lastIndexOf('-location') + 9 == possibleContexts[i].name.length) &&
            (action == null))
        {
            //is the context field
            contextXPath = possibleContexts[i].value;
            break;
        }
        else if ((possibleContexts[i].type == 'hidden') &&
            (possibleContexts[i].name.indexOf('xgf_hyfrepeat-') == 0) &&
            (possibleContexts[i].name.lastIndexOf('-location' + action) + (action.length) + 9 == possibleContexts[i].name.length) &&
            (action != null))
        {
            //is the context field
            contextXPath = possibleContexts[i].value;
            break;
        }
    }

    var newXPath = xpathString;

    //can only adjust if xpath starts with context
    if (xpathString.indexOf(contextXPath) == 0)
    {
        var remainder = xpathString.substring(contextXPath.length);
        if (remainder.indexOf('[') == 0)
        {
            var closingIndex = remainder.indexOf(']');

            if (typeof(predicateValue) == 'undefined')
            {
                var number = remainder.substring(1, closingIndex);
                number = parseInt(number) - 1;
                newXPath = contextXPath + '[' + number + remainder.substring(closingIndex);
            }
            else
            {
                newXPath = contextXPath + '[' + predicateValue + remainder.substring(closingIndex);
            }

            //alert(xpathString + '\n' + newXPath);
        }
    }

    return newXPath;
}

/**
 * Updates the value of the repeat count hidden field for the given table,
 * by either adding or removing 1 from the current value.
 * @param table The HTML table object representing the repeat to adjust the count for.
 * @param change either 'add' or 'remove' to indicate whetehr to add 1 to the count, or to reduce it by 1.
 * @private
 */
hyf.editabletable.manageCount = function(table, change)
{
    var possibleCounts = table.parentNode.getElementsByTagName('input');
    for (var i = 0; i< possibleCounts.length; ++i)
    {
        if ((possibleCounts[i].type == 'hidden') &&
            (possibleCounts[i].name.indexOf('hyfrepeat-') == 0) &&
            (possibleCounts[i].name.lastIndexOf('-count') + 6 == possibleCounts[i].name.length))
        {
            //is the count field
            var currentCount = parseInt(possibleCounts[i].value);
            if (change == 'get')
                return currentCount;
            else if (change == 'add')
                currentCount++;
            else
                currentCount--;

            possibleCounts[i].value = currentCount;
            break;
        }
    }
}

/**
 * Inserts a new row into the table using the contents of the new row fields.
 * @param tableId The ID of the table to insert the new row into.
 */
hyf.editabletable.insertRow = function(tableId)
{
    if (hyf.editabletable.currentRowId != null)
        hyf.editabletable.cancelChanges(hyf.editabletable.currentRowId);

    var newRow = document.getElementById(tableId + '_newRowContent').rows[0];

    //check if we need to validate first
    if (hyf.editabletable.optionsCollection[tableId].validate)
    {
        if (hyf.validation.validateContainer(newRow))
        {
            hyf.editabletable.insertRowImpl(tableId);
        }
    }
    else
    {
        hyf.editabletable.insertRowImpl(tableId);
    }
}

/**
 * Actual implementaion of the insertRow functionality
 * @param tableId The ID of the table to insert the new row into.
 */
hyf.editabletable.insertRowImpl = function(tableId)
{
    var table = document.getElementById(tableId);

    var currentCount = hyf.editabletable.manageCount(table, 'get');
    var newCount = parseInt(currentCount) + 1;
    var existingIdPrefix = tableId + 'BlankEntry';
    var newIdPrefix = tableId + newCount;

    var newRow = document.getElementById(tableId + '_newRowContent').rows[0];

    //create new HTML row
    var blankRow = document.createElement('tr');
    blankRow.setAttribute('id', newRow.getAttribute('id'));
    newRow.setAttribute('id', hyf.editabletable.replaceString(newRow.getAttribute('id'), existingIdPrefix, newIdPrefix));
    blankRow.className = newRow.className;

    dojo.removeClass(newRow, hyf.editabletable.classes.insertRow);

    //special handling for radio/checkbox controls in IE6 to ensure the values do not get lost on insert
    if (dojo.isIE < 8)
    {
        dojo.query("input[type=radio], input[type=checkbox]", newRow).forEach(function(node) {
            if (node.checked)
                node.defaultChecked = true;
        });
    }

    document.getElementById(tableId + '_body').appendChild(newRow);
    document.getElementById(tableId + '_newRowContent').appendChild(blankRow);

    for (var i = 0; i < newRow.cells.length; ++i)
    {
        var existingCell = newRow.cells[i];
        if (existingCell.getAttribute('id') != tableId + '_insert')
        {
            var newCell = document.createElement('td');
            for (var att = 0; att < existingCell.attributes.length; ++att)
            {
                var attObj = existingCell.attributes[att];
                if ((attObj.nodeName == 'class') && (dojo.isIE < 8))
                    newCell.className = attObj.nodeValue;
                else
                    newCell.setAttribute(attObj.nodeName, attObj.nodeValue);
            }
            newCell.cssText = existingCell.cssText;

            var cellId = existingCell.getAttribute('id')

            var ihHidden = document.getElementById(cellId + hyf.editabletable.HTML_CONTENT_ID_END);
            var ihClone = ihHidden.cloneNode(true);


            //if there are any dojo widgets we need to revert back to standard HTML before trying to change ids
            if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function') && (dijit.findWidgets(existingCell).length > 0))
            {
                //revert widget to HTML
                var currentValue = hyf.editabletable.getCellValue(existingCell, 'data');
                //Destroy any dojo widgets in the row being inserted before processing
                hyf.util.destroyDojoWidgets(existingCell);
                existingCell.innerHTML = ihHidden.value;
                //update value in the new HTML
                hyf.editabletable.setCellValue(existingCell, currentValue);
            }
            else
                ihHidden.parentNode.removeChild(ihHidden);


            hyf.editabletable.changeIds(existingCell, existingIdPrefix, newIdPrefix);

            blankRow.appendChild(newCell);

            //update the content of the blank row cell to revert to the intitial HTML
            if (ihClone != null)
            {
                newCell.innerHTML = ihClone.value;
                newCell.appendChild(ihClone);
            }
        }
        else
            blankRow.appendChild(existingCell);
    }

    //Create any dojo widgets in the new blank row.
    dojo.parser.parse(blankRow);
    hyf.validation.initiateWidgets(blankRow);

    //set binding field xpaths for the new row
    for (var i = 0; i < newRow.cells.length; ++i)
    {
        hyf.editabletable.processXPath(table, newRow.cells[i], newCount);
    }

    //convert new row to display only
    hyf.editabletable.processRow(newRow);

    //Recreate any dojo widgets in the new inserted row.
    dojo.parser.parse(newRow);
    hyf.validation.initiateWidgets(newRow);
    //update the display only values for those cells that contain dojo widgets now they have been parsed
    if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function'))
    {
        var widgets = dijit.findWidgets(newRow);
        for(var i = 0; i < widgets.length; ++i)
        {
            hyf.editabletable.updateDisplayOnlyValue(hyf.editabletable.findParentElement(widgets[i].domNode, 'td'));
        }
    }

    //increase repeat count field
    hyf.editabletable.manageCount(table, 'add');

    hyf.editabletable.callChangeFunction(newRow.getAttribute('id'), 'insert');
}

/**
 * Calls the change function defined for this table (if any) passing through the details of the change
 */
hyf.editabletable.callChangeFunction = function(rowId, type)
{
    var row = document.getElementById(rowId);
    if (row != null)
    {
        var table = hyf.editabletable.findParentTable(row);
        var cf = hyf.editabletable.optionsCollection[table.getAttribute('id')].change_function;
        if (cf != null)
        {
            if (typeof(cf) == 'string')
            {
                var evalString = cf + '("' + type + '", "' + rowId + '")';
                eval(evalString);
            }
            else if (typeof(cf) == 'function')
            {
                cf(type, rowId);
            }
        }
    }
}

/**
 * Replaces all of the occurances of the 'match' string in the 'str' string
 * with the contents of the 'replace' string.
 * @private
 */
hyf.editabletable.replaceString = function(str, match, replace)
{
    //alert('replacing string ' + match + ' with ' + replace + ' in ' + str);
    var re = new RegExp(match, 'g');
    return str.replace(re, replace);
}

/**
 * Finds the last table object that has been output to the document.
 * This is used as the page is being rendered to initialise any ediatbale tables
 */
hyf.editabletable.getLastTableOutput = function()
{
    var elem = document.lastChild;
    while (elem.lastChild != null)
    {
        elem = elem.lastChild;
    }
    if (elem.nodeType != 1)
    {
        var prevElem = hyf.util.getPreviousElementSibling(elem);
        if (prevElem == null)
        {
            elem = elem.parentNode;
        }
        else
        {
            elem = prevElem;
        }
    }
    return elem.parentNode.getElementsByTagName('table')[0];
}



/**
 * Checks if the given field is actually contained with an insert new entry row
 * for an editable table control.
 * @param field The HTML field to check
 * @return boolean value, true of the field is within a new entry row, false otherwise.
 */
hyf.editabletable.checkNewRowField = function(field)
{
    if (dojo.hasClass(field, hyf.editabletable.classes.insertRow))
    {
        return true;
    }
    else if (field.parentElement != null)
    {
        return hyf.editabletable.checkNewRowField(field.parentElement);
    }
    else
    {
        return false;
    }
}



/**
 * When first displayed, any dojo widgets will not yet have been rendered.
 * Therefore, the display only value shown will not have been converted by the widget
 * eg The currency widget adds the $ sign at the start, and formats the number.
 * This method goes through all the tables after the dojo widgets have been parsed,
 * and updates the displayed value for any widget found.
 */
hyf.editabletable.processInitialValues = function()
{
    if ((typeof(dijit) != 'undefined') && (typeof(dijit.findWidgets) == 'function'))
    {
        for (tableId in hyf.editabletable.optionsCollection)
        {
            var table = document.getElementById(tableId);

            hyf.editabletable.processInitialValuesForTable(table);
        }
    }
}

hyf.editabletable.processInitialValuesForTable = function(table)
{
    var widgets = dijit.findWidgets(table);

    for (var i = 0; i < widgets.length; ++i)
    {
        var w = widgets[i];

        var td = hyf.editabletable.findParentElement(w.domNode, 'td');

        hyf.editabletable.updateDisplayOnlyValue(td);
    }
}

/**
 * This method updates the display only value in the given cell to make
 * sure that it matches that stored in the editable control in the cell.
 * This assumes that the cell has already been processed by the processRow method
 * above to create the editable and display only spans.
 * @param cell The td representing the cell to update
 */
hyf.editabletable.updateDisplayOnlyValue = function(cell)
{
    var displayValue = hyf.editabletable.getCellValue(cell);
    if (displayValue == null || "undefined" == typeof(displayValue) || displayValue.length == 0)
            displayValue = "&nbsp;";

    //find the display span for this cell
    var spans = cell.getElementsByTagName('span');
    for (var i = 0; i < spans.length; ++i)
    {
        var spanId = spans[i].getAttribute('id');
        if ((spanId != null) && (spanId.lastIndexOf(hyf.editabletable.DISPLAY_ID_END) + hyf.editabletable.DISPLAY_ID_END.length == spanId.length))
        {
            spans[i].innerHTML = displayValue;
            break;
        }
    }
}


//we need the HTML content of the edit rows before dojo has converted it to widgets,
//so that we can adjust the ids, and then parser it to create the widgets for each new row.
//Therefore we have to delay the usual onload parsing for dojo widgets.  Instead we add a new onload call
//to call the dojo parser at the end of any other onload calls (eg table initImpl above).
dojo.config.parseOnLoad = false;
dojo.addOnLoad(function(){
    dojo.addOnLoad(function() {
            dojo.parser.parse();
            hyf.editabletable.processInitialValues();
    });
});