// ***********************************************************************************
// ** Grid client events                                                         *****
// https://www.igniteui.com/help/infragistics.web.mvc~infragistics.web.mvc.gridselectionclientevents_members
// ***********************************************************************************



// https://www.igniteui.com/help/infragistics.web.mvc~infragistics.web.mvc.gridselectionclientevents~cellselectionchanging
// Event fired before cell(s) are about to be selected (cancellable). 
// Return false in order to cancel cell selection changing.
// Function takes arguments evt and ui.
// Use ui.owner to get reference to igGridSelection.
// Use ui.owner.grid to get reference to igGrid.
// Use ui.cell.element to get reference to cell DOM element.
// Use ui.cell.columnKey to get reference to column key.
// Use ui.cell.index to get column index.
// Use ui.cell.row to get reference to row DOM element.
// Use ui.cell.rowIndex to get row index.
// Use ui.selectedCells to get reference to selected cells object array.ui.manual - returns true if internal trigger.

function GridCellSelectionChanged(evt, ui) {

    if (!evt || !ui)
        return;
    if (!ui.owner)
        return;
    if (!ui.selectedCells)
        return;

    var FirstSelectedCell = ui.selectedCells[0];
    if (!FirstSelectedCell)
        return;

    var GridId = evt.target.id;
    //ToastMessage("GridCellSelectionChanged: " + GridId);
    var IgGrid = $("#" + GridId);
    $('input[id="HiddenSelectedGridId"]').val(GridId);

    //https://www.igniteui.com/help/api/2017.1/ui.iggrid_hg?_ga=2.67289875.306346588.1649440140-1366708258.1643751367&_gac=1.220269162.1648830688.CjwKCAjwxZqSBhAHEiwASr9n9I0c8ZymzLk5EuqMS7D8iCNrmmcr1iDXAgcA2RQcqAd60zLvJWkqnxoCszMQAvD_BwE#methods:getCellValue
    var Grid = ui.owner;
    var ColumnKey = FirstSelectedCell.columnKey;
    var SelectedRow = FirstSelectedCell.row;
    var RowIndex = FirstSelectedCell.rowIndex;
    var RowKey = GetRowKeyFromElement(SelectedRow);

    //console.log("GridCellSelectionChanged RowKey: " + RowKey);

    if (!RowKey)
        return;

/*
    // Find the actual SPAN child inside this TD
    // This SPAN will contain attributes to identify the database record
    var sSelectedCellDbTable = "";
    var sSelectedCellDbColumn = "";
    var sGridDbTable = IgGrid.attr('data-dbtable');
    if (FirstSelectedCell && FirstSelectedCell.element) {
        var span = $(FirstSelectedCell.element).find('[data-dbcolumn]');
        if (span) {
            // Special attribute for the record db column (for accessing the database)
            sSelectedCellDbTable = $(span).attr('data-dbtable');
            sSelectedCellDbColumn = $(span).attr('data-dbcolumn');
        }
    }

    // What is selected?
    var SelectedRows = $("#" + GridId).igGridSelection("selectedRows");
    var SelectedCells = $("#" + GridId).igGridSelection('selectedCells');

    // Set the selected record into hidden values
    // The server will read these values for update and display
    var RecordTableName = Grid.grid.getCellValue(RowKey, "COL_DBTABLE");
    var RecordId = Grid.grid.getCellValue(RowKey, "GUID");
     if (RecordTableName && RecordId) {
        $('input[id="HiddenSelectedDataTypeId"]').val("stdata.BaseDataSet");
        $('input[id="HiddenSelectedTableId"]').val(RecordTableName);
        $('input[id="HiddenSelectedRecordId"]').val(RecordId);
        $('input[id="HiddenSelectedColumnId"]').val(ColumnKey);
        $('input[id="HiddenSelectedCellTableId"]').val(sSelectedCellDbTable);
        $('input[id="HiddenSelectedCellColumnId"]').val(sSelectedCellDbColumn);
    }
*/

    var bValuesSet = SetHiddenRecordValuesFromSelectedCell(GridId, FirstSelectedCell);
    if (bValuesSet) {
        // Don't take over the small mobile phone screen 
        if (!bSmallSizeApp) {
            // New selected record!
            // Trigger Ajax function to update properties grid
            RefreshPropertyGridViewFromHiddenSelection($("#"+PropertyGridPartialViewDivId));
        }
    }

    // Only do this for full-sized app because cell editing is too confusing when tapping on cellphone
    var bIsStartEditOnClick = IsGridSelectionStartEditOnClick(GridId);
    if (bIsStartEditOnClick) {
        OnContentEdit(GridId);
        return;
    } else {
        // Popup a small modeless dialog to assist in editing the selected cell
        // Click "Edit" button to go into edit text edit mode
        // Click "Delete" button to delete the selected cell contents (normally for timesheet entries)
        setTimeout(function () { GridCellSelectionChangedPopup(evt, GridId); }, 200);
    }
}


function IsGridSelectionStartEditOnClick(GridId) {

    // Search for class: starteditonclick
    var Grid = $("#" + GridId);
    if (!Grid)
        return false;
    if (Grid.length == 0)
        return false;

    // Never edit on click on cellphone. Too confusing and too small
    if (bSmallSizeApp)
        return false;

    // What is selected?
    var SelectedRows = $(Grid).igGridSelection("selectedRows");
    var SelectedCells = $(Grid).igGridSelection('selectedCells');
    var FirstSelectedCell = null;
    if (SelectedCells != null && SelectedCells.length > 0)
        FirstSelectedCell = SelectedCells[0];
    if (!FirstSelectedCell)
        return false;

    var FirstSelectedCellObject = $(FirstSelectedCell);
    if (!FirstSelectedCellObject)
        return false;
    if (FirstSelectedCellObject.length == 0)
        return false;
    var FirstSelectedCellId = FirstSelectedCellObject.attr('id');

    // See ModelRecordsetGrid.cs for "timesheettimeperiodcell starteditonclick"
    // Special cells that accept a click for editing
    // This makes editing timesheet cells easier, but protects other cells from possible deletion
    var bIsStartEditOnClick = FirstSelectedCellObject.hasClass("starteditonclick");
    if (!bIsStartEditOnClick) {
        var ChildItems = $(FirstSelectedCell.element).find(".starteditonclick");
        bIsStartEditOnClick = (ChildItems && ChildItems.length > 0);
    }
    return bIsStartEditOnClick;
}

function SetHiddenRecordValuesFromSelectedCell(GridId, FirstSelectedCell) {

    if (!GridId)
        return false;
    if (!FirstSelectedCell)
        return false;

    try {
        var GridObject = $("#" + GridId);
        if (!GridObject || GridObject.length == 0)
            return false;

        var ColumnKey = FirstSelectedCell.columnKey;
        var SelectedRow = FirstSelectedCell.row;
        var RowIndex = FirstSelectedCell.rowIndex;
        var RowKey = GetRowKeyFromElement(SelectedRow);
        if (!RowKey)
            return;

        // Find the actual SPAN child inside this TD
        // This SPAN will contain attributes to identify the database record
        var sSelectedCellDbTable = "";
        var sSelectedCellDbColumn = "";
        var sGridDbTable = GridObject.attr('data-dbtable');
        if (FirstSelectedCell && FirstSelectedCell.element) {
            var span = $(FirstSelectedCell.element).find('[data-dbcolumn]');
            if (span) {
                // Special attribute for the record db column (for accessing the database)
                sSelectedCellDbTable = $(span).attr('data-dbtable');
                sSelectedCellDbColumn = $(span).attr('data-dbcolumn');
            }
        }

        // What is selected?
        var SelectedRows = $("#" + GridId).igGridSelection("selectedRows");
        var SelectedCells = $("#" + GridId).igGridSelection('selectedCells');

        // Set the selected record into hidden values
        // The server will read these values for update and display
        var IgGrid = GridObject.igGrid();
        var RecordTableName = GridObject.igGrid("getCellValue", RowKey, "COL_DBTABLE");
        var RecordId = GridObject.igGrid("getCellValue", RowKey, "GUID");

        // Show properties grid
        if (RecordTableName && RecordId) {
            $('input[id="HiddenSelectedDataTypeId"]').val("stdata.BaseDataSet");
            $('input[id="HiddenSelectedTableId"]').val(RecordTableName);
            $('input[id="HiddenSelectedRecordId"]').val(RecordId);
            $('input[id="HiddenSelectedColumnId"]').val(ColumnKey);
            $('input[id="HiddenSelectedCellTableId"]').val(sSelectedCellDbTable);
            $('input[id="HiddenSelectedCellColumnId"]').val(sSelectedCellDbColumn);

            // Values have been set
            return true;
        }
    }
    catch (e) {
        console.log("SetHiddenRecordValuesFromSelectedCell Error (ColumnKey:'" + ColumnKey + "', RowKey:'" + RowKey + "')");
        console.log(e);
    }

    // No selected record
    return false;
}


function SetHiddenRecordValuesFromSelection() {

    var GridId = $('input[id="HiddenSelectedGridId"]').val();
    if (!GridId)
        return false;
    var GridObject = $("#" + GridId);
    if (!GridObject)
        return false;

    // Any seelction
    var SelectedCells = GridObject.igGridSelection('selectedCells');
    if (!SelectedCells)
        return false;
    if (!SelectedCells.length)
        return false;

    var FirstSelectedCell = SelectedCells[0];
    return SetHiddenRecordValuesFromSelectedCell(GridId, FirstSelectedCell);
}




/**
 * Get all the selected cells
 * @param {any} gridid
 */
function GetSelectedGridCells(gridid) {
    var SelectedCells = $("#" + gridid).igGridSelection('selectedCells');
    return SelectedCells;
}


/**
 * Select an entire row by GUID
 * @param {any} guid
 */
function SelectGridRowById(gridid, sRowId) {

    if (!gridid)
        return;
    if (!sRowId)
        return;

// Grid is not configured to select entire rows
    //$("#" + gridid).igGridSelection("selectRowById", sRowId);

    //// Test results
    //var SelectedRows = $("#" + gridid).igGridSelection("selectedRows");
    //var SelectedCells = $("#" + gridid).igGridSelection('selectedCells');

    SelectAllGridRowCellsById(gridid, sRowId);
}

function SelectAllGridRowCellsById(gridid, sRowId) {

    if (!gridid)
        return;
    if (!sRowId)
        return;

    try {
        var GridRow = $("#" + gridid).igGrid("rowById", sRowId);
        if (!GridRow)
            return;

        var HtmlRowElement = GridRow[0];
        if (!HtmlRowElement)
            return;
        if (!HtmlRowElement || !HtmlRowElement.cells)
            return;
        var RowIndex = HtmlRowElement.rowIndex;
        var Cells = HtmlRowElement.cells;
        if (!Cells || Cells.length == 0)
            return;

        // Select each cell individually
        for (var i = 0; i < Cells.length; i++) {
            var HtmlCell = Cells[i];
            var CellIndex = HtmlCell.cellIndex;
            $("#" + gridid).igGridSelection("selectCell", RowIndex, CellIndex);

            // Ugly compromise
            // Only select one cell because grid scrolls horizontally to the last column (confusing to user)
            break;
        }

        // Test results
        var SelectedRows = $("#" + gridid).igGridSelection("selectedRows");
        var SelectedCells = $("#" + gridid).igGridSelection('selectedCells');
    }
    catch (e) {
    }
}

function SelectGridCellByRowId(gridid, sRowId, sColumnKey) {

    if (!gridid)
        return;
    if (!sRowId)
        return;
    if (!sColumnKey)
        return;

    $("#" + gridid).igGridSelection("selectCellById", sRowId, sColumnKey);

    // Test results
    var SelectedRows = $("#" + gridid).igGridSelection("selectedRows");
    var SelectedCells = $("#" + gridid).igGridSelection('selectedCells');

    // Scroll into view, ensurevisible
    // DOM Element: Ensure visible
    var CellElement;
    if (SelectedRows && SelectedRows.length > 0 && SelectedRows[0].element && SelectedRows[0].element.length > 0)
        CellElement = SelectedRows[0].element[0];
    else if (SelectedCells && SelectedCells.length > 0 && SelectedCells[0].element && SelectedCells[0].element.length > 0)
        CellElement = SelectedCells[0].element[0];

    // This doesn't work well. The element is scrolled to the bottom of the grid, which is not the best place for it
    //ScrollIntoViewIfNeeded(CellElement);
}






var bIsGridInEditMode = false;
var GridStartedEditMode = new Date();
function IsGridInEditMode() {

    if (!bIsGridInEditMode)
        return false;

    // Still in edit mode after several minutes?
    // That's suspicious...
    var Now = new Date();
    var millisecondsSinceEditStarted = Now.getTime() - GridStartedEditMode.getTime();
    if (millisecondsSinceEditStarted > (2/*mins*/ * 60 * 1000))
        return false;

    //ToastError("In edit mode");

    // Still in edit mode
    return true;
}

// EditCellStarted
// Event which is raised after start cell editing.
// Function takes arguments evt and ui.
// Use ui.owner to get reference to igGridUpdating.
// Use ui.owner.grid to get reference to igGrid.
// Use ui.rowID to get key or index of row.
// Use ui.columnIndex to get index of column.
// Use ui.columnKey to get key of column.
// Use ui.editor to get reference to igEditor.
// Use ui.value to get value of editor.
// Use ui.rowAdding to check if that event is raised while new-row-adding.
// https://www.igniteui.com/help/api/2021.2/ui.iggridupdating.html
function GridEditCellStarted(evt, ui) {

    bIsGridInEditMode = true;
    GridStartedEditMode = new Date();
    var GridId = evt.target.id;

    //ToastError("Start edit mode");
}


// EditCellEnded
// Event which is raised after end cell editing.
// Function takes arguments evt and ui.
// Use ui.owner to get reference to igGridUpdating.
// Use ui.owner.grid to get reference to igGrid.
// Use ui.rowID to get key or index of row.
// Use ui.columnIndex to get index of column.
// Use ui.columnKey to get key of column.
// Use ui.editor to get reference to igEditor.
// Use ui.value to get value of cell.
// Use ui.oldValue to get old value.
// Use ui.update to check if cell was modified and data source will be updated.
// Use ui.rowAdding to check if that event is raised while new-row-adding.
// https://www.igniteui.com/help/api/2021.2/ui.iggridupdating.html
function GridEditCellEnded(evt, ui) {

    bIsGridInEditMode = false;
    GridStartedEditMode = new Date();

    //ToastError("End edit mode");

    // Any change?
    if (!ui.update)
        return;
    if (!evt.target.id)
        return;
    if (!ui.rowID)
        return;
    if (!ui.columnKey)
        return;

    var GridId = evt.target.id;
    var Grid = $("#" + GridId);

    // Find the cell TD element by row id and column key
    var cell = Grid.igGrid("cellById", ui.rowID, ui.columnKey);
    if (!cell)
        return;

    // Find the actual SPAN child inside this TD
    // This SPAN will contain attributes necessary to update the database record
    var span = $(cell).find('[data-dbcolumn]');
    if (!span)
        return;

    // Special attribute for the record db column (for accessing the database)
    var sDbTable = $(span).attr('data-dbtable');
    var sDbColumn = $(span).attr('data-dbcolumn');
    if (!sDbTable)
        return;
    if (!sDbColumn)
        return;

    // Yellow cells have been edited
    $(cell).addClass("editedgridcell");

    var RecordId = ui.rowID;

    //ToastMessage("Old: " + ui.oldValue + "<br>New: " + ui.value + "<br>DbTable: " + sDbTable + "<br>DbColumn: " + sDbColumn + "<br>Row: " + ui.rowID);

    var ContentType = GetSelectedContentType();
    AjaxSetContentGridValue(ui.value, ui.oldValue, ContentType, sDbTable, sDbColumn, RecordId, false/*IgnorePrompts*/);
}


function GetDbTableFromRowCol(gridid, rowid, colkey) {

    try {
        var GridObj = $("#" + gridid);

        // Find the cell TD element by row id and column key
        var cell = GridObj.igGrid("cellById", rowid, colkey);
        if (!cell)
            return;

        // Find the actual SPAN child inside this TD
        // This SPAN will contain attributes necessary to update the database record
        var span = $(cell).find('[data-dbtable]');
        if (!span)
            return;

        var sDbTable = $(span).attr('data-dbtable');
        return sDbTable;
    }
    catch (e) {

    }

    return undefined;
}




/**
 * Check to see if this grid value can be set.
 * Prompts may appear first
 * Example: This date range is locked. Would you like to bypass the lock?
 * After prompts are satisfied the value can be set
 * @param {any} NewValue
 * @param {any} ContentType
 * @param {any} RecordTableName
 * @param {any} RecordDbColumn
 * @param {any} RecordId
 */
function AjaxSetContentGridValue(NewValue, OldValue, ContentType, RecordTableName, RecordDbColumn, RecordId, IgnorePrompts) {

    var FilterJsonString = GetSelectedContentPaneFilterJsonString();

    var params = {
        "NewValue": NewValue,
        "OldValue": OldValue,
        "ContentType": ContentType,
        "TableName": RecordTableName,
        "DbColumn": RecordDbColumn,
        "RecordId": RecordId,
        "IgnorePrompts": IgnorePrompts,
        "FilterJsonString": FilterJsonString,
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxSetContentGridValue",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {

            if (response.Success) {

                // Has the value been set?
                if (response.ValueSet) {
                    // Select this row after rebuild
                    SetSelectRecordIdAfterRebuild(response.ObjectTable, response.ResultRecordId);
                    DeferRefreshSelectedContent();
                    return;
                }

                var bCanSetValueAfterPrompts = (response.CanSetValue);

                // Are there prompts to satisfy before the value can be set?
                // If so, open the prompts and then set the value after OK or YES is pressed in each
                if (response.PromptListJsonString) {
                    var PromptArray = JSON.parse(response.PromptListJsonString);
                    if (PromptArray && PromptArray.length > 0) {

                        // No longer in edit mode
                        bIsGridInEditMode = false;

                        // Clearing the grid here is kind-of rude, but necessary if the prompt is answered NO
                        // When NO is clicked there is no callback to clear the grid
                        // This should probably be improved in the future with a "cancelproceedcallback" for prompts
                        // TODO: P8 "cancelproceedcallback"
                        RefreshSelectedContent();

                        // Start the prompts
                        if (bCanSetValueAfterPrompts)
                             Prompts(PromptArray, SetContentGridValueAfterPrompts, params);
                        else Prompts(PromptArray);
                    }
                }

            }
            else {
                ToastError("SetContentPropertyValue", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("SetContentPropertyValue", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });

}


/**
 * Called after SetContentGridValue when all prompts have been satisfied
 * The last prompt callback calls this function
 * @param {any} previouspromptresults
 * @param {any} ParamVars
 */
function SetContentGridValueAfterPrompts(previouspromptresults, ParamVars) {

    // ParamVars has these values from the previous Ajax call
        //var params = {
        //    "NewValue": NewValue,
        //    "OldValue": OldValue,
        //    "ContentType": ContentType,
        //    "TableName": RecordTableName,
        //    "DbColumn": RecordDbColumn,
        //    "RecordId": RecordId,
        //    "IgnorePrompts": IgnorePrompts
        //};

    // Prompts have been satisfied
    // Call again, but ignore prompts this time
    AjaxSetContentGridValue(ParamVars.NewValue, ParamVars.OldValue, ParamVars.ContentType, ParamVars.TableName, ParamVars.DbColumn, ParamVars.RecordId, true/*IgnorePrompts*/);
}





/**
 * Delete all the selected content records
 * */
function CheckPromptThenDeleteMultipleContentRecords() {

    // Check the current grid for multiple selection
    var GridId = $('input[id="HiddenSelectedGridId"]').val();
    if (!GridId)
        return false;
    var GridObject = $("#" + GridId);
    if (!GridObject)
        return false;

    // Multiple items selected
    var SelectedGridItems = GetSelectedIdsFromGridId(GridId);
    if (!SelectedGridItems)
        return false;
    if (!SelectedGridItems.SelectedTableIds)
        return false;
    if (!SelectedGridItems.SelectedRowIds)
        return false;

    // Must have multiple items
    // At least 2 records
    if (SelectedGridItems.SelectedTableIds.length < 2)
        return false;
    if (SelectedGridItems.SelectedRowIds.length < 2)
        return false;

    CurrentSelectedDbTableCsvIds = "";
    CurrentSelectedDbRecordCsvIds = "";

    // Prepare to delete
    for (i = 0; i < SelectedGridItems.SelectedTableIds.length; ++i) {

        if (i < SelectedGridItems.SelectedTableIds.length) {
            if (CurrentSelectedDbTableCsvIds)
                CurrentSelectedDbTableCsvIds += ",";
            CurrentSelectedDbTableCsvIds += SelectedGridItems.SelectedTableIds[i];
        }
        if (i < SelectedGridItems.SelectedRowIds.length) {
            if (CurrentSelectedDbRecordCsvIds)
                CurrentSelectedDbRecordCsvIds += ",";
            CurrentSelectedDbRecordCsvIds += SelectedGridItems.SelectedRowIds[i];
        }
    }

    // Anything to delete?
    if (!CurrentSelectedDbTableCsvIds.length)
        return;
    if (!CurrentSelectedDbRecordCsvIds.length)
        return;

    // Prompt
    var nSelectedItems = SelectedGridItems.SelectedTableIds.length;
    var sPromptMsg = "Are you sure you wish to delete all " + nSelectedItems + " items?\r\n\r\nThis cannot be undone.";
    PopupYesNoWithDoubleVerification("Delete Multiple Items", sPromptMsg, ProceedDeleteMultipleContentRecords, null/*proceedobject*/);

    // Asked to delete selected items
    return true;
}

function ProceedDeleteMultipleContentRecords() {
    AjaxDeleteMultipleContentRecords(CurrentSelectedDbTableCsvIds, CurrentSelectedDbRecordCsvIds);
}

var CurrentSelectedDbTableCsvIds = "";
var CurrentSelectedDbRecordCsvIds = "";

function AjaxDeleteMultipleContentRecords(DbTableIds, DbRecordIds) {

    if (!DbTableIds)
        return;
    if (!DbRecordIds)
        return;
    if (!DbTableIds.length)
        return;
    if (!DbRecordIds.length)
        return;

    var ContentType = GetSelectedContentType();

    var params = {
        "ContentType": ContentType,
        "DbTableIds": DbTableIds,
        "DbRecordIds": DbRecordIds,
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxDeleteMultipleContentRecords",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {
            if (response.Success) {

                // Clear these so we don't come back here
                CurrentSelectedDbTableCsvIds = "";
                CurrentSelectedDbRecordCsvIds = "";

                // Something to say?
                if (response.DeleteResults)
                    ToastMessage("Delete Items", response.DeleteResults);
                if (response.PromptTitle && response.PromptMessage)
                    PopupMessageBox(response.PromptTitle, response.PromptMessage);

                RefreshSelectedContent();
            }
            else if (response.ErrorResults) {
                ToastError("AjaxDeleteMultipleContentRecords", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("AjaxDeleteMultipleContentRecords", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });

}





// https://www.igniteui.com/help/infragistics.web.mvc~infragistics.web.mvc.gridselectionclientevents~rowselectionchanged
// Event fired after row(s) are selected.
// Function takes arguments evt and ui.
// Use ui.owner to get reference to igGridSelection.
// Use ui.owner.grid to get reference to igGrid.
// Use ui.row.element to get reference to row DOM element.
// Use ui.row.index to get row index.
// Use ui.selectedRows to get reference to rows object array.ui.manual - returns true if internal trigger.

function GridRowSelectionChanged(evt, ui) {
    //alert('GridRowSelectionChanged');
}


function GridActiveCellChanged(evt, ui) {

    if (!evt || !ui)
        return;
    if (!ui.owner)
        return;
    var GridId = evt.target.id;

    //console.log("GridActiveCellChanged: " + event.clientX);
}






/**
 * Delete the select recordset
 * Ajax will get the proper delete command to execute
 * Examples
 *      1. Are you sure you wish to delete the time log
 *      2. Are you sure you wish to delete the project
 * */
function OnDeleteSelectedContentRecord() {

    // Multiple items selected?
    if (CheckPromptThenDeleteMultipleContentRecords())
        return;

    // Single item selected
    // Delete the selected record in the active grid
    // First get the selected recordset
    var SelectedDataType = $('input[id="HiddenSelectedDataTypeId"]').val();
    var SelectedTableId = $('input[id="HiddenSelectedTableId"]').val();
    var SelectedRecordId = $('input[id="HiddenSelectedRecordId"]').val();

    // No hidden values? Set them from the current selection
    if (!SelectedTableId || !SelectedRecordId) {

        // Try getting values from selection
        SetHiddenRecordValuesFromSelection();

        // Try again
        SelectedDataType = $('input[id="HiddenSelectedDataTypeId"]').val();
        SelectedTableId = $('input[id="HiddenSelectedTableId"]').val();
        SelectedRecordId = $('input[id="HiddenSelectedRecordId"]').val();
    }

    // Search deeper for recordsetvaluelist attribues
    // These occur in timesheets where multiple values are aggregated into single cells
    // See stdata.RecordSetListValue.GetJsonValues()
    var GridId = $('input[id="HiddenSelectedGridId"]').val();
    if (GridId) {
        var GridObj = $("#" + GridId);
        if (GridObj && GridObj.length) {
            var RecordsetValueList = GetRecordsetListValueFromSelectedCells(GridObj);
            if (RecordsetValueList) {
                if (RecordsetValueList.Table && RecordsetValueList.ID) {
                    SelectedTableId = RecordsetValueList.Table;
                    SelectedRecordId = RecordsetValueList.ID;
                }
            }
        }
    }

    // Get a recordset?
    if (!SelectedTableId || !SelectedRecordId) {

        ToastError("Delete", "You must first select a row or item. Then click to delete.");
        return;
    }

    // Execute stdata.MenuCmdItem.CMD_DELETE
    // Are you sure you want to delete the project?
    AjaxExecuteDeleteMenuCmd(SelectedDataType, SelectedTableId, SelectedRecordId);
}





/**
 * Open a context menu for the selected recordset
 * */
function OnSelectedContentGridOptionsMenu() {

    // Open an options menu for the selected grid record
    // First get the selected recordset
    var SelectedDataType = $('input[id="HiddenSelectedDataTypeId"]').val();
    var SelectedTableId = $('input[id="HiddenSelectedTableId"]').val();
    var SelectedRecordId = $('input[id="HiddenSelectedRecordId"]').val();
    var sSelectedCellDbTable = $('input[id="HiddenSelectedCellTableId"]').val();
    var sSelectedCellDbColumn = $('input[id="HiddenSelectedCellColumnId"]').val();

    // No hidden values? Set them from the current selection
    if (!SelectedTableId || !SelectedRecordId) {

        // Try getting values from selection
        SetHiddenRecordValuesFromSelection();

        // Try again
        SelectedDataType = $('input[id="HiddenSelectedDataTypeId"]').val();
        SelectedTableId = $('input[id="HiddenSelectedTableId"]').val();
        SelectedRecordId = $('input[id="HiddenSelectedRecordId"]').val();
        sSelectedCellDbTable = $('input[id="HiddenSelectedCellTableId"]').val();
        sSelectedCellDbColumn = $('input[id="HiddenSelectedCellColumnId"]').val();
    }

    // Get a recordset?
    if (!SelectedTableId || !SelectedRecordId) {
    //    ToastError("Options Menu", "You must first select a row or item. Then click to see an options menu.");
    //    return;
    }

    // Open content menu
    AjaxGetObjectContextMenu(event, $(this), SelectedDataType, SelectedTableId, SelectedRecordId, sSelectedCellDbTable, sSelectedCellDbColumn);
}

function GetSelectedRecordId() {

    var SelectedRecordId = $('input[id="HiddenSelectedRecordId"]').val();
    return SelectedRecordId;
}

function GetSelectedRecordTableId() {

    var SelectedTableId = $('input[id="HiddenSelectedTableId"]').val();
    return SelectedTableId;
}

function GetSelectedDataType() {

    var SelectedDataType = $('input[id="HiddenSelectedDataTypeId"]').val();
    return SelectedDataType;
}

function GetSelectedColumnId() {

    var SelectedColumnId = $("#HiddenSelectedColumnId").val();
    return SelectedColumnId;
}





function OnFilterGrid() {

    ShowFilterPane();

    // Filter the selected grid
    ToastMessage('Filter', 'Click tree items to filter this content');
}


/**
 * Double-click on grid row
 * @param {any} GridElement
 * @param {any} event
 */
function OnGridClickEvent(GridElement, event) {

    if (!GridElement)
        return;
    if (!event)
        return;
    if (!event.target)
        return;

    var GridId = GridElement.attr("id");

    var bIsStartEditOnClick = IsGridSelectionStartEditOnClick(GridId);
    if (bIsStartEditOnClick) {
        OnContentEdit(GridId);
        return;
    } else {
        // Popup a small modeless dialog to assist in editing the selected cell
        // Click "Edit" button to go into edit text edit mode
        // Click "Delete" button to delete the selected cell contents (normally for timesheet entries)
        setTimeout(function () { GridCellSelectionChangedPopup(event, GridId); }, 200);
    }
}


/**
 * Double-click on grid row
 * @param {any} GridElement
 * @param {any} event
 */
function OnGridDoubleClickEvent(GridElement, event) {

    if (!GridElement)
        return;
    if (!event)
        return;
    if (!event.target)
        return;

    event.preventDefault();

    // https://www.igniteui.com/help/api/2021.2/ui.iggrid

    var IgGrid = GridElement.igGrid();
    var ClickedCellHtmlElement = event.target;
    if (!ClickedCellHtmlElement)
        return;
    if (!ClickedCellHtmlElement.parentElement)
        return;
    if (!ClickedCellHtmlElement.parentElement.parentElement)
        return;

    var ClickedCellElement = $(ClickedCellHtmlElement);
    var ClickedRowHtmlElement = ClickedCellHtmlElement.parentElement.parentElement;
    var ClickedRowElement = $(ClickedRowHtmlElement);

    //var RowKey = ClickedRowElement.attr('data-id');
    var RowKey = GetRowKeyFromElement(ClickedRowHtmlElement);
    if (!RowKey)
        return;

    // Find the actual SPAN child inside this TD
    // This SPAN will contain attributes to identify the database record
    var sSelectedCellDbTable = "";
    var sSelectedCellDbColumn = "";
    if (ClickedRowHtmlElement) {
        var span = $(ClickedRowHtmlElement).find('[data-dbcolumn]');
        if (span) {
            // Special attribute for the record db column (for accessing the database)
            sSelectedCellDbTable = $(span).attr('data-dbtable');
            sSelectedCellDbColumn = $(span).attr('data-dbcolumn');
        }
    }

    var IgClickedRow = GridElement.igGrid("rowById", RowKey);
    var RecordTableName = GridElement.igGrid("getCellValue", RowKey, "COL_DBTABLE");

    var ObjectDataType = "stdata.BaseDataSet";
    var ObjectTable = RecordTableName;
    var ObjectRecordId = RowKey;

    // Fill properties with partial view from server
    AjaxRefreshPropertyGridView($("#"+PropertyGridPartialViewDivId), "OBJECT", ObjectDataType, ObjectTable, ObjectRecordId, sSelectedCellDbTable, sSelectedCellDbColumn, ""/*PropertyValuesJsonString*/);
    AddPropertiesGridToContentArray(ObjectDataType, ObjectTable, ObjectRecordId, sSelectedCellDbTable, sSelectedCellDbColumn);

    // Show the panel
    ShowPropertiesPane();
}



/**
 * Find the data-id from the grid row this element is in
 * @param {any} RowElement
 */
function GetRowKeyFromElement(ClickedElement) {

    if (!ClickedElement)
        return null;

    // Try this element. Maybe it is a row
    // If not, go up through the parents until a row is found
    ClickedElement = $(ClickedElement);
    if (!ClickedElement)
        return null;

    var RowKey = ClickedElement.attr('data-id');

    // Go up until a row is found
    var nLoops = 0;
    while (!RowKey) {
        ClickedElement = ClickedElement.parent();
        if (!ClickedElement)
            return null;

        ClickedElement = $(ClickedElement);
        if (!ClickedElement)
            return null;

       RowKey = ClickedElement.attr('data-id');

        // No infinite loop
        nLoops++;
        if (nLoops > 30)
            return null;
    }

    return RowKey;
}



function GetRowObjectFromElement(ClickedElement) {

    if (!ClickedElement)
        return null;

    var RowObject = $(ClickedElement);

    // Go up until a row is found
    var nLoops = 0;
    while (RowObject) {

        if (RowObject.is('tr'))
            return RowObject;

        RowObject = RowObject.parent();
        if (!RowObject)
            return null;

        // No infinite loop
        nLoops++;
        if (nLoops > 30)
            return null;
    }

    return null;
}


function GetColumnObjectFromElement(ClickedElement) {

    if (!ClickedElement)
        return null;

    var ColumnObject = $(ClickedElement);

    // Go up until a row is found
    var nLoops = 0;
    while (ColumnObject) {

        if (ColumnObject.is('th'))  // Table header
            return ColumnObject;
        if (ColumnObject.is('td'))  // Table cell
            return ColumnObject;

        ColumnObject = ColumnObject.parent();
        if (!ColumnObject)
            return null;

        // No infinite loop
        nLoops++;
        if (nLoops > 30)
            return null;
    }

    return null;
}



/**
 * Dig inside a partial view for the grid
 * @param {any} PartialViewDivId
 */
function FindGridIdInsidePartialViewDiv(PartialViewDivId) {

    if (!PartialViewDivId)
        return null;
    var PartialViewElement = $('#' + PartialViewDivId);
    if (!PartialViewElement)
        return null;

    var GridElement = $(PartialViewElement).find('[data-grid=true]');
    if (!GridElement)
        return null;

    var GridId = GridElement.attr("id");
    return GridId;
}



/**
 * Grid is fully rendered
 * @param {any} event
 * @param {any} ui
 */
function OnGridRendered(event, ui) {

    if (!ui)
        return;
    if (!ui.owner) // reference to igGrid
        return;

    var Grid = ui.owner;
    var GridObj = $(ui.owner);
    if (!GridObj)
        return;
    if (GridObj.length == 0)
        return;
    var GridId = event.target.id;
    if (!GridId)
        return;
    var GridObject = $("#" + GridId);
    if (!GridObject)
        return;
    if (GridObject.length == 0)
        return;

    // Save this
    $('input[id="HiddenSelectedGridId"]').val(GridId);

    // Remember the scroll position between refreshes
    // Does not work
    //$("#" + GridId).data("igGrid")._persistVirtualScrollTop = true;

    // Special grid attributes, after grid has rendered
    CollapseGridRows(GridObject);
    SetGridRowStyles(GridObject);
    BuildGanttElementsByGrid(GridObject);
    SetColumnWidthsFromCookies(GridObject);
    SetupGridHeaderClickHandler(GridObject);
    SetupGridHeaderContextMenuHandler(GridObject);

    // Remember last scroll position
    setTimeout(function () { SetupGridScrollEvent(GridId); }, 100);

    // Select records after rebuild
    CheckSelectGridItemsAfterRebuild(GridObject);

    // Fix columns last so all rows are correct height
    SetColumnFixingFromCookies(GridObject);

    // Do this last, after all grid setup is finished
    SetLastGridScrollPosition(GridId);
}




// Remember grid scroll position for each grid type
var GridScrollTopDictionary = {};
var GridScrollLeftDictionary = {};

function SetupGridScrollEvent(GridId) {

    var GridScrollContainer = $("#" + GridId + "_scroll");
    if (!GridScrollContainer || !GridScrollContainer.length)
        return;

    // Each type of grid has its own scroll positions (in different pages in the UI)
    ContentType = GetSelectedContentType();

    // Remember the postion after each scroll
    $(GridScrollContainer).on("scroll", function () {
        GridScrollTopDictionary[ContentType] = GridScrollContainer.scrollTop();
        GridScrollLeftDictionary[ContentType] = GridScrollContainer.scrollLeft();
        //ToastMessage("Scroll", GridScrollContainer.scrollTop() + ", " + GridScrollContainer.scrollLeft());
    });
}

function SetLastGridScrollPosition(GridId) {

    var GridScrollContainer = $("#" + GridId + "_scroll");
    if (!GridScrollContainer || !GridScrollContainer.length)
        return;

    // Each type of grid has its own scroll positions (in different pages in the UI)
    ContentType = GetSelectedContentType();

    // Set the position after grid is rendered
    $(GridScrollContainer).scrollTop(GridScrollTopDictionary[ContentType]);
    $(GridScrollContainer).scrollLeft(GridScrollLeftDictionary[ContentType]);
    //ToastMessage("Set Scroll", GridScrollContainer.scrollTop() + ", " + GridScrollContainer.scrollLeft());
}




/**
 * Collapse rows after grid has rendered
 * @param {any} GridElement
 */
function CollapseGridRows(GridElement) {

    var RecordIds = GetCollapsedRecordIdsCookieAsArray();
    if (!RecordIds || RecordIds.length == 0)
        return;

    // Collapse each record
    for (const sRecordId of RecordIds) {
        CollapseRowById(GridElement, sRecordId);
    }
}


/**
 * Set row colors, fonts, columns background colors
 * @param {any} GridElement
 * @returns
 */
function SetGridRowStyles(GridElement) {

    try {
        if (!GridElement)
            return;
        if (GridElement.length == 0)
            return;

        // Find hidden string containing grid row styles
        var GridParent = GridElement.parent().parent().parent();
        var HiddenRowStyles = GridParent.find('.HiddenRowStyles');
        if (!HiddenRowStyles)
            return;
        if (HiddenRowStyles.length == 0)
            return;

        // Hidden value contains a Json string
        var RowStylesJsonString = HiddenRowStyles.val();
        if (!RowStylesJsonString)
            return;
        if (RowStylesJsonString.length == 0)
            return;

        // Json string is an array of row styles
        var GridRowStylesArray = JSON.parse(RowStylesJsonString);
        if (!GridRowStylesArray)
            return;
        if (GridRowStylesArray.length == 0)
            return;

        //console.log(GridRowStylesArray.length);

        // Iternate over array of grid styles
        for (i = 0; i < GridRowStylesArray.length; ++i) {
            var GridRowStyle = GridRowStylesArray[i];
            var sRecordId = GridRowStyle.RecordId;
            var GridRow = FindGridRowByRecordId(GridElement, sRecordId);
            if (!GridRow)
                continue;
            if (GridRow.length == 0)
                continue;

            // Background color
            if (GridRowStyle.HasBkColor)
                GridRow.css("background-color", "rgb(" + GridRowStyle.BkRed + ", " + GridRowStyle.BkGreen + ", " + GridRowStyle.BkBlue + ")");

            // Text color
            if (GridRowStyle.HasTextColor)
                GridRow.css("color", "rgb(" + GridRowStyle.TxRed + ", " + GridRowStyle.TxGreen + ", " + GridRowStyle.TxBlue + ")");

            // Font style
            if (GridRowStyle.HasFontStyle) {
                if (GridRowStyle.Bold)
                    GridRow.css("font-weight", "700");
                if (GridRowStyle.Italic)
                    GridRow.css("font-style", "italic");
                if (GridRowStyle.Underline)
                    GridRow.css("text-decoration", "underline");
                if (GridRowStyle.Strikeout)
                    GridRow.css("text-decoration", "line-through");
            }

            if (GridRowStyle.HasHeightMultiplier) {
                //var RowHeight = GridRow.innerHeight();
                //GridRow.css("height", (GridRowStyle.HeightMultiplier * RowHeight) + "px");
                GridRow.css("font-weight", "700");
                //var fontsizestyle = window.getComputedStyle(GridRow[0], null).getPropertyValue('font-size');
                //var fontSize = parseFloat(fontsizestyle);
                //GridRow.css("font-size", (/*GridRowStyle.HeightMultiplier*/ 1.25 * fontSize) + "px");
            }

            // Column background colors
            if (GridRowStyle.sColmBkClrs) {
                // Format: DbColumnName:IDX:HexColor
                SetGridRowColumnStyles(GridElement, GridRow, GridRowStyle.sColmBkClrs);
            }
        }
    }
    catch (e) {
    }
}

function SetGridRowColumnStyles(GridElement, GridRow, sColmBkClrs) {

    // CSV list of column background colors
    if (!GridRow || GridRow.length == 0)
        return;
    if (!sColmBkClrs)
        return;

    // Multiple colors are slit by comma
    const ColorsPartsArray = sColmBkClrs.split(",");
    if (!ColorsPartsArray)
        return;
    if (ColorsPartsArray.length < 1)
        return;

    for (const sColmBkClr of ColorsPartsArray) {

        // The color string has values separated by colon character
        // Example: % Status:3:FFAA00
        const ValuesPartsArray = sColmBkClr.split(":");
        if (!ValuesPartsArray)
            return;
        if (ValuesPartsArray.length < 3)
            return;

        // Values contained in color string
        var sDbColumn = ValuesPartsArray[0];
        var sColumnIdx = ValuesPartsArray[1];
        var sColumnHexColor = ValuesPartsArray[2];

        // Find the column cell
        var ColumnDivObject = GridRow.find("[data-dbcolumn='" + sDbColumn + "']");

        // Set the column cell background color
        if (ColumnDivObject && ColumnDivObject.length > 0) {
            ColumnDivObject.css("background-color", "#" + sColumnHexColor);
            ColumnDivObject.css("color", "#000000");
            ColumnDivObject.parent().css("background-color", "#" + sColumnHexColor);
            ColumnDivObject.parent().css("color", "#000000");
        }
    }
}





//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
// Select these GUIDs after the partial grid view is refreshed                                                                                                                          //
//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
var SelectGridItemsAfterRebuild = {};
function CheckSelectGridItemsAfterRebuild(element) {

    try {
        if (!SelectGridItemsAfterRebuild)
            return;

        // Special attribute holding the grid id for selection
        if (!element)
            return;
        var GridId = $(element).attr("data-gridid");
        if (!GridId)
            return;

        // Convert record ids to cells
        if (SelectGridItemsAfterRebuild && SelectGridItemsAfterRebuild.SelectedRowIds) {
            for (var i = 0; i < SelectGridItemsAfterRebuild.SelectedRowIds.length; i++) {
                var sTableId = SelectGridItemsAfterRebuild.SelectedTableIds[i];
                var sRecordId = SelectGridItemsAfterRebuild.SelectedRowIds[i];

                var GridCellArray = SearchForGridCellsByGuid(GridId, sRecordId);
                if (GridCellArray && GridCellArray.length > 0)
                    SelectGridItemsAfterRebuild.SelectedCellIdKeys = GridCellArray;
            }
        }

        // Reselect each cell
        if (SelectGridItemsAfterRebuild && SelectGridItemsAfterRebuild.SelectedCellIdKeys) {
            for (var i = 0; i < SelectGridItemsAfterRebuild.SelectedCellIdKeys.length; i++) {
                var CellIdKey = SelectGridItemsAfterRebuild.SelectedCellIdKeys[i];
                if (CellIdKey) {
                    var sRowId = CellIdKey.RowId;
                    var ColumnKey = CellIdKey.ColumnKey;
                    $('input[id="HiddenSelectedRecordId"]').val(sRowId);
                    SelectGridCellByRowId(GridId, sRowId, ColumnKey);
                }
            }
        }

        // Is there a selection? If so, set hidden values used by property grid
        SetHiddenRecordValuesFromSelection();

        // Clear this, do not come back here
        SelectGridItemsAfterRebuild = {};

        // Show properties for new selection
        // Don't take over the small mobile phone screen 
        if (!bSmallSizeApp) {
            // New selected record!
            // Trigger Ajax function to update properties
            RefreshPropertyGridViewFromHiddenSelection($("#" + PropertyGridPartialViewDivId));
        }
    }
    catch (e) {
    }
}



function GetSelectedIdsFromPartialViewDiv(PartialViewDivId) {

    // These GUIDs will likely be reselected after the view is refreshed
    // See CheckSelectGridItemsAfterRebuild()

    // Dig into the partial view for the grid
    // If this is not a grid, then return null because there is no selection
    try {
        if (!PartialViewDivId)
            return null;
        var GridId = FindGridIdInsidePartialViewDiv(PartialViewDivId);
        if (!GridId)
            return null;

        return GetSelectedIdsFromGridId(GridId);
    }
    catch (e) {
    }

    // Return the current selection
    return SelectedGridItems;
}



function GetSelectedIdsFromGridId(GridId) {

    // These GUIDs will likely be reselected after the view is refreshed
    // See CheckSelectGridItemsAfterRebuild()

    // Dig into the partial view for the grid
    // If this is not a grid, then return null because there is no selection
    try {
        if (!GridId)
            GridId = $('input[id="HiddenSelectedGridId"]').val();
        if (!GridId)
            return null;
        var GridObj = $("#" + GridId);
        if (!GridObj)
            return null;

        // What is selected?
        var SelectedRows = $(GridObj).igGridSelection("selectedRows");
        var SelectedCells = $(GridObj).igGridSelection('selectedCells');

        // Create a new selection object to hold the current rows and cells
        var SelectedGridItems = {};
        SelectedGridItems.SelectedTableIds = [];
        SelectedGridItems.SelectedRowIds = [];
        SelectedGridItems.SelectedCellIdKeys = [];
        SelectedGridItems.SelectedDbTableCsvIds = "";
        SelectedGridItems.SelectedDbRecordCsvIds = "";

        // Copy the selected rows
        if (SelectedRows && SelectedRows.length > 0) {
            for (var i = 0; i < SelectedRows.length; i++) {
                var SelectedRow = SelectedRows[i];
                var RowId = SelectedRow.attr('data-id');
                if (RowId) {
                    var RowTableId = SelectedRow.attr('data-dbtable');
                    SelectedGridItems.SelectedTableIds.push(RowTableId);
                    SelectedGridItems.SelectedRowIds.push(RowId);

                    // Add to table id csv string
                    if (SelectedGridItems.SelectedDbTableCsvIds)
                        SelectedGridItems.SelectedDbTableCsvIds += ",";
                    SelectedGridItems.SelectedDbTableCsvIds += RowTableId;

                    // Add to record id csv string
                    if (SelectedGridItems.SelectedDbRecordCsvIds)
                        SelectedGridItems.SelectedDbRecordCsvIds += ",";
                    SelectedGridItems.SelectedDbRecordCsvIds += RowId;
                }
            }
        }

        // Copy the selected cells
        if (SelectedCells && SelectedCells.length > 0) {
            for (var i = 0; i < SelectedCells.length; i++) {
                var SelectedCell = SelectedCells[i];
                var RowId = SelectedCell.id;
                var ColumnKey = SelectedCell.columnKey;
                if (RowId && ColumnKey) {
                    var CellIdKey = { RowId, ColumnKey };
                    SelectedGridItems.SelectedCellIdKeys.push(CellIdKey);

                    var RowTableId = GetDbTableFromRowCol(GridId, RowId, ColumnKey);
                    if (RowTableId) {
                        SelectedGridItems.SelectedTableIds.push(RowTableId);
                        SelectedGridItems.SelectedRowIds.push(RowId);

                        // Add to table id csv string
                        if (SelectedGridItems.SelectedDbTableCsvIds)
                            SelectedGridItems.SelectedDbTableCsvIds += ",";
                        SelectedGridItems.SelectedDbTableCsvIds += RowTableId;

                        // Add to record id csv string
                        if (SelectedGridItems.SelectedDbRecordCsvIds)
                            SelectedGridItems.SelectedDbRecordCsvIds += ",";
                        SelectedGridItems.SelectedDbRecordCsvIds += RowId;
                    }
                }
            }
        }
    }
    catch (e) {
        var sSelectError = e.name + ": " + e.message;
    }

    // Return the current selection
    return SelectedGridItems;
}

/**
 * Find all the grid cells that represent this record id
 * */
function SearchForGridCellsByGuid(GridId, sRecordId) {

    GridCellsArray = [];
    if (!GridId)
        return GridCellsArray;
    if (!sRecordId)
        return GridCellsArray;

    var GridObj = $("#" + GridId);
    if (!GridObj)
        return GridCellsArray;
    if (!GridObj.length)
        return GridCellsArray;

    // Find all the <span> objects with this record id
    var ElementObjs = GridObj.find("[data-guid='" + sRecordId + "']");
    if (!ElementObjs || !ElementObjs.length) {

        // Check for stdata.BaseDataSet.RecordSetListValue cell containing several values
        // This occurs in the Timesheet where values are aggregated into single cells
        // Look for this ID in the list of values in timesheet cell using a "*= partial" string search
        // https://api.jquery.com/category/selectors/attribute-selectors/
        // Selects elements that have the specified attribute with a value containing a given substring
        ElementObjs = GridObj.find("[data-recordsetlistvaluejson*='" + sRecordId + "']");
        if (!ElementObjs || !ElementObjs.length)
            return GridCellsArray;
    }


    // Find the <td> above each <span>
    for (i = 0; i < ElementObjs.length; ++i) {
        var Element = ElementObjs[i];
        if (!Element)
            continue;
        var ElementObj = $(Element);
        if (!ElementObj)
            continue;
        if (!ElementObj.length)
            continue;

        // Find <td> above <span>
        var TdObj = ElementObj.closest('td');
        if (!TdObj)
            continue;
        if (!TdObj.length)
            continue;

        // Find <tr> above <td>
        var TrObj = TdObj.closest('tr');
        if (!TrObj)
            continue;
        if (!TrObj.length)
            continue;

        var ColumnKey = TdObj.attr('id');
        if (!ColumnKey)
            ColumnKey = TdObj.attr('aria-describedby');
        ColumnKey = ColumnKey.replace(GridId + "_", "");

        var RowId = TrObj.attr('id');
        if (!RowId)
            RowId = TrObj.attr('data-id');
        RowId = RowId.replace(GridId + "_", "");
        var CellIdKey = { RowId, ColumnKey };

        // Add a new cell to the list
        GridCellsArray.push(CellIdKey);
    }

    // Return all the cells for this record id
    return GridCellsArray;
}



var SelectTableIdAfterRebuild = "";
var SelectRecordIdAfterRebuild = "";
/**
 * Select a single row after rebuild
 * @param {any} sRecordId
 */
function SetSelectRecordIdAfterRebuild(sDbTable, sRecordId) {

    if (!sRecordId)
        return;

    if (!sDbTable)
        sDbTable = GetSelectedRecordTableId();
    SelectTableIdAfterRebuild = sDbTable;
    SelectRecordIdAfterRebuild = sRecordId;

    //console.log("SetSelectRecordIdAfterRebuild: " + sDbTable + ", " + sRecordId);
}




function GetRecordsetListValueFromSelectedCells(GridElement) {

    var RecordsetValueListArray = GetRecordsetListValueArrayFromSelectedCells(GridElement);
    if (!RecordsetValueListArray)
        return null;
    if (!Array.isArray(RecordsetValueListArray))
        return null;
    if (RecordsetValueListArray.length == 0)
        return null;

    // Use first item only
    var RecordsetValueList = RecordsetValueListArray[0];
    return RecordsetValueList;
}


function GetRecordsetListValueArrayFromSelectedCells(GridElement) {

    // Any grid selection?
    var SelectedCells = $(GridElement).igGridSelection('selectedCells');
    if (!SelectedCells)
        return null;
    if (!SelectedCells.length)
        return null;

    // Try each selected cell for a value
    for (i = 0; i < SelectedCells.length; ++i) {

        var SelectedCell = SelectedCells[i];
        if (!SelectedCell)
            continue;
        if (!SelectedCell.element)
            continue;

        var RecordsetListValueArray = GetRecordsetListValueArrayFromElement(SelectedCell.element);
        if (RecordsetListValueArray)
            return RecordsetListValueArray;
    }

    // No value
    return null;
}


function GetRecordsetListValueArrayFromElement(element) {

    if (!element)
        return null;

    // Json parsing could throw error
    try {
        var ElementObject = $(element);
        if (!ElementObject)
            return null;
        if (ElementObject.length == 0)
            return null;

        // Check for stdata.BaseDataSet.RecordSetListValue cell containing several values
        // This occurs in the Timesheet where values are aggregated into single cells
        var ResultsObjs = ElementObject.find("[data-recordsetlistvaluejson]");
        if (!ResultsObjs)
            return null;
        if (ResultsObjs.length == 0)
            return null;

        var RecordsetListValueJson = ResultsObjs.attr("data-recordsetlistvaluejson");
        if (!RecordsetListValueJson)
            return null;
        var RecordsetListValues = JSON.parse(RecordsetListValueJson);
        if (!RecordsetListValues)
            return null;
        if (!RecordsetListValues.Values)
            return null;

        // Array of RecordsetListValue objects
        return RecordsetListValues.Values;
    }
    catch (e) {
        ToastError("RecordsetListValueArray", e.name + ": " + e.message);
    }

    // No recordset list value array in this element
    return null;
}



function GridColumnIndexFromKey(Grid, ColumnKey) {

    var columns = $(Grid).igGrid("option", "columns");
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].key == ColumnKey) {
            return i;
        }
    }

    // Not found
    return 0;
}


/**
 * Edit the selected grid cell
 * */
function OnContentEdit(GridId) {

    try {
        if (!GridId)
            GridId = GridEditPopupGridId;
        if (!GridId)
            return;
        var Grid = $("#" + GridId);
        if (!Grid)
            return;

        // What is selected?
        var SelectedRows = $(Grid).igGridSelection("selectedRows");
        var SelectedCells = $(Grid).igGridSelection('selectedCells');
        var FirstSelectedCell = null;
        if (SelectedCells != null && SelectedCells.length > 0)
            FirstSelectedCell = SelectedCells[0];
        var ColumnKey = FirstSelectedCell.columnKey;
        var SelectedRow = FirstSelectedCell.row;
        var RowIndex = FirstSelectedCell.rowIndex;
        var RowKey = GetRowKeyFromElement(SelectedRow);
        if (!ColumnKey)
            return;
        if (!RowKey)
            return;

        // Popup may be in the way
        CloseGridEditPopupDialog();

        // Start edit
        // https://www.igniteui.com/help/api/2016.2/ui.iggridupdating?_ga=2.47534127.921551204.1656082072-1526114389.1655224915#methods:startEdit
        bIsGridInEditMode = true;
        GridStartedEditMode = new Date();
        $(Grid).igGridUpdating('startEdit', RowKey, ColumnKey);

        //var e = jQuery.Event("keyup");
        //e.which = 113; // F2, enters edit mode
        //e.keyCode = 113;
        //e.key = 113;
        //$(Grid).focus();
        //$(Grid).trigger(e);
    }
    catch (e) {
    }
}

/**
 * Edit the selected grid cell
 * */
function OnContentDelete() {

    if (!GridEditPopupGridId)
        return;
    var Grid = $("#" + GridEditPopupGridId);
    if (!Grid)
        return;

    OnDeleteSelectedContentRecord();
}


function OnContentProperties() {

    if (!GridEditPopupGridId)
        return;
    var Grid = $("#" + GridEditPopupGridId);
    if (!Grid)
        return;

    RefreshPropertyGridViewFromHiddenSelection($("#"+PropertyGridPartialViewDivId));
    ShowPropertiesPane();
}


// OLD
//function AjaxCheckEditContentCellOnClick(GridId) {

//    if (!GridId)
//        return;
//    var Grid = $("#" + GridId);
//    if (!Grid)
//        return;
//    if (Grid.length == 0)
//        return;

//    // What is selected?
//    var SelectedRows = $(Grid).igGridSelection("selectedRows");
//    var SelectedCells = $(Grid).igGridSelection('selectedCells');
//    var FirstSelectedCell = null;
//    if (SelectedCells != null && SelectedCells.length > 0)
//        FirstSelectedCell = SelectedCells[0];
//    var ColumnKey = FirstSelectedCell.columnKey;
//    var SelectedRow = FirstSelectedCell.row;
//    var RowIndex = FirstSelectedCell.rowIndex;
//    var RowKey = GetRowKeyFromElement(SelectedRow);
//    if (!ColumnKey)
//        return;
//    if (!RowKey)
//        return;

//    var ContentType = GetSelectedContentType();

//    var params = {
//        "ContentType": ContentType,
//        "RowKey": RowKey,
//        "ColumnKey": ColumnKey,
//    };

//    $.ajax({
//        url: VirtualDirectory + "/Home/AjaxCheckEditContentCellOnClick",
//        type: "POST",
//        data: JSON.stringify(params),
//        contentType: "application/json",
//        success: function (response) {
//            if (response.Success) {

//                if (response.EditOnClick) {
//                    OnContentEdit(GridId);
//                }
//            }
//            else {
//                ToastError("AjaxCheckEditContentCellOnClick", response.ErrorResults);
//            }
//        },
//        error: function (xhr, status, error) {
//            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
//            ToastError("AjaxCheckEditContentCellOnClick", errormsg);
//            console.log('AJAX: ' + errormsg);
//        }
//    });
//}



// https://www.igniteui.com/help/api/2022.1/ui.igGridResizing#events:columnResized
//columnResized
// Cancellable: false
//Event fired after the resizing has been executed and results are rendered.

//    evt
//Type: Event
//JQuery event object.

//    ui
//Type: Object
//owner
//Type: Object
//Gets a reference to the GridResizing widget.

//    owner.grid
//Type: Object
//Gets a reference to the grid widget.

//    columnIndex
//Type: Number
//Gets the resized column index.

//    columnKey
//Type: String
//Gets the resized column key.

//    originalWidth
//Type: Number
//Gets the original column width.

//    newWidth
//Type: Number
//Gets the final column width after resizing.

function GridColumnResized(evt, ui) {

    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return;

    // Save column width to cookie
    SaveColumnWidthToCookie(ContentType, ui.columnKey, ui.newWidth);
}

function GridColumnResizing(evt, ui) {

    // Must rebuild because Gantt link lines cannot be moved
    RebuildGanttLinkLinesFromSelectedContentPane();
}



function SetColumnWidthsFromCookies(GridObject) {

    if (!GridObject)
        return;
    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return;

    // Check each column for a cookie (containing the width for that column)
    // https://www.igniteui.com/help/api/2018.2/ui.iggridresizing

    var columns = GridObject.igGrid("option", "columns");
    var columnIndex = 0;
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].hidden)
            continue;

        // Got a width cookie?
        var ColumnWidth = LoadColumnWidthFromCookie(ContentType, columns[i].key);
        if (ColumnWidth) {
            // Resize the column
            GridObject.igGridResizing("resize", columnIndex, ColumnWidth);
        }

        columnIndex++;
    }

    //GridObject.igGridResizing("resize", 0, 1000);
}



function SetColumnFixingFromCookies(GridObject) {

    if (!GridObject)
        return;
    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return;

    // Check each column for a cookie (containing the width for that column)
    var columns = GridObject.igGrid("option", "columns");
    var columnIndex = 0;
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].hidden)
            continue;

        // Got a "fixed" cookie?
        var bColumnFixed = LoadColumnFixedFromCookie(ContentType, columns[i].key);
        if (bColumnFixed) {
            // Resize the column
            GridObject.igGridColumnFixing("fixColumn", columns[i].key, true);
        }

        columnIndex++;
    }

    // This is an ugly hack to fix up row heights between "fixed" and "unfixed" grid areas
    SyncGridRowHeights(GridObject);
}


function UnfixAllColumns(GridObject) {

    if (!GridObject)
        return;

    // Unfix all columns so rows can be collapsed and expanded without strange side-effects
    var columns = GridObject.igGrid("option", "columns");
    var columnIndex = 0;
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].hidden)
            continue;

        // Unfix column
        GridObject.igGridColumnFixing("unfixColumn", columns[i].key, true);

        columnIndex++;
    }
}


function HasAnyFixedColumns(GridObject) {

    if (!GridObject)
        return false;
    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return false;

    // Check each column for a cookie (containing the width for that column)
    var columns = GridObject.igGrid("option", "columns");
    var columnIndex = 0;
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].hidden)
            continue;

        // Got a "fixed" cookie?
        var bColumnFixed = LoadColumnFixedFromCookie(ContentType, columns[i].key);
        if (bColumnFixed)
            return true;

        columnIndex++;
    }

    // No fixed columns for this grid
    return false;
}

function SyncGridRowHeights(GridObject) {

    if (!GridObject)
        return;

// Doesn't seem to do anything.
// Rows are still misaligned on some browsers
    // This is an ugly hack to fix up row heights between "fixed" and "unfixed" grid areas
    // The Infragistics grid evidently cannot do this on it's own
    //GridObject.igGridColumnFixing("syncHeights", true, true);

    // More ugly hacks, IMO
    //var fixedRows = GridObject.igGrid("fixedRows");
    //var unfixedRows = GridObject.igGrid("rows");
    //GridObject.igGridColumnFixing("syncRowsHeights", fixedRows, unfixedRows);
}




function GetColumnIndexByKey(GridElement, columnKey) {
    var columns = GridElement.igGrid("option", "columns");
    var columnIndex = 0;
    for (var i = 0; i < columns.length; i++) {
        if (columns[i].hidden)
            continue;
        if (columns[i].key === columnKey) {
            return columnIndex;
        }
        columnIndex++;
    }
    return -1;
}



/**
 * Insert a new column into the grid
 * @param {any} params
 */
function InsertGridColumn(params) {

    var InsertGridDbColumn = ExtractDbColumnFromParams(params);
    var InsertAtColumnIndex = ExtractColumnIndexFromParams(params);

    var ContentType = GetSelectedContentType();
    var LayoutString = LoadSelectedPaneLayoutStringFromCookie();
    AjaxInsertGridColumn(ContentType, LayoutString, InsertGridDbColumn, InsertAtColumnIndex);
}

/**
 * Hide a grid column
 * @param {any} params
 */
function HideGridColumn(params) {

    // Got a column?
    var HideGridDbColumn = ExtractDbColumnFromParams(params);
    var HideGridColumnIndex = ExtractColumnIndexFromParams(params);

    var ContentType = GetSelectedContentType();
    var LayoutString = LoadSelectedPaneLayoutStringFromCookie();
    AjaxHideGridColumn(ContentType, LayoutString, HideGridDbColumn, HideGridColumnIndex);
}


function AjaxHideGridColumn(ContentType, LayoutString, HideGridDbColumn, HideGridColumnIndex) {

    if (typeof HideGridColumnIndex === 'undefined')
        HideGridColumnIndex = -1;

    if (!HideGridDbColumn && HideGridColumnIndex < 0)
        return;

    var params = {
        "ContentType": ContentType,
        "LayoutString": LayoutString,
        "HideGridDbColumn": HideGridDbColumn,
        "HideGridColumnIndex": HideGridColumnIndex
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxHideGridColumn",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {
            if (response.Success) {

                // Save and refresh the content
                SaveSelectedPaneLayoutStringToCookie(response.NewLayoutString);
                RefreshSelectedContent();
            }
            else {
                ToastError("AjaxHideGridColumn", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("AjaxHideGridColumn", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });
}


function AjaxInsertGridColumn(ContentType, LayoutString, InsertGridDbColumn, InsertAtColumnIndex) {

    if (typeof InsertAtColumnIndex === 'undefined')
        InsertAtColumnIndex = 1;

    if (!InsertGridDbColumn)
        return;

    var params = {
        "ContentType": ContentType,
        "LayoutString": LayoutString,
        "InsertGridDbColumn": InsertGridDbColumn,
        "InsertAtColumnIndex": InsertAtColumnIndex
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxInsertGridColumn",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {
            if (response.Success) {

                if (response.NewLayoutString) {
                    // Save and refresh the content
                    SaveSelectedPaneLayoutStringToCookie(response.NewLayoutString);
                    RefreshSelectedContent();
                }
            }
            else {
                ToastError("AjaxInsertGridColumn", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("AjaxInsertGridColumn", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });
}


function AjaxSortGridColumn(ContentType, LayoutString, SortGridDbColumn, SortGridColumnIndex) {

    if (typeof SortGridColumnIndex === 'undefined')
        SortGridColumnIndex = -1;

    if (!SortGridDbColumn && SortGridColumnIndex < 0)
        return;

    var params = {
        "ContentType": ContentType,
        "LayoutString": LayoutString,
        "SortGridDbColumn": SortGridDbColumn,
        "SortGridColumnIndex": SortGridColumnIndex
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxSortGridColumn",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {
            if (response.Success) {

                if (response.NewLayoutString) {
                    // Save and refresh the content
                    SaveSelectedPaneLayoutStringToCookie(response.NewLayoutString);
                    RefreshSelectedContent();
                }
            }
            else {
                ToastError("AjaxSortGridColumn", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("AjaxSortGridColumn", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });
}


function ExtractDbColumnFromParams(params) {

    if (!params)
        return null;
    if (!Array.isArray(params))
        return null;
    if (params.length == 0)
        return null;

    for (let i = 0; i < params.length; ++i) {
        // The first parameter is the name
        // The next parameter is the actual value
        var param = params[i];

        // Array inside this array?
        if (Array.isArray(param)) {
            var sReturn = ExtractDbColumnFromParams(param);
            if (typeof sReturn !== 'undefined')
                return sReturn;
        }

        if (param === "DbColumn"/*stdata.MenuCmdItem.PARAM_DBCOLUMN*/)
            return params[i + 1];

        // Skip the actual value
        i++;
    }
}

function ExtractColumnIndexFromParams(params) {

    if (!params)
        return null;
    if (!Array.isArray(params))
        return null;
    if (params.length == 0)
        return null;

    for (let i = 0; i < params.length; ++i) {
        // The first parameter is the name
        // The next parameter is the actual value
        var param = params[i];

        // Array inside this array?
        if (Array.isArray(param)) {
            var sReturn = ExtractColumnIndexFromParams(param);
            if (typeof sReturn !== 'undefined')
                return sReturn;
        }

        if (param === "ColumnIdx"/*stdata.MenuCmdItem.PARAM_COLUMNINDEX*/)
            return params[i + 1];

        // Skip the actual value
        i++;
    }
}


function DefaultGridColumns() {

    // Promt for confirmation, then revert all grid columns to default
    var prompt = { sPrompt: "Are you sure you wish to remove all the customized columns and revert to defaults?", sTitle : "Revert Columns to Defaults"};
    Prompt(prompt, ClearGridLayout/*proceedcallback*/, null/*proceedobject*/)
}


function ClearGridLayout() {
    try {
        // Revert all grid columns to defaults
        // Save a blank layout string (default columns)
        ClearSelectedPaneColumnWidths();
        SaveSelectedPaneLayoutStringToCookie("");
        RefreshSelectedContent();
    } catch (e) {
        ToastError("ClearGridLayout", e.name + ": " + e.message + "\n" + e.stack);
    }
}









var sCollapseRockerBase64UrlString;
var sExpandRockerBase64UrlString;

function AjaxSetCollapseExpandImg(ImgElementObj, bCollapseImg) {

    if (sCollapseRockerBase64UrlString && sExpandRockerBase64UrlString) {
        ImgElementObj.removeAttr("data-image");
        ImgElementObj.attr("src", bCollapseImg ? sCollapseRockerBase64UrlString : sExpandRockerBase64UrlString);
        return;
    }

    var params = {
    };

    $.ajax({
        url: VirtualDirectory + "/Home/AjaxGetCollapseExpandImages",
        type: "POST",
        data: JSON.stringify(params),
        contentType: "application/json",
        success: function (response) {
            if (response.Success) {

                // Cache valuse for later use
                sCollapseRockerBase64UrlString = response.CollapseRockerBase64UrlString;
                sExpandRockerBase64UrlString = response.ExpandRockerBase64UrlString;

                if (sCollapseRockerBase64UrlString && sExpandRockerBase64UrlString) {
                    ImgElementObj.removeAttr("data-image");
                    ImgElementObj.attr("src", bCollapseImg ? sCollapseRockerBase64UrlString : sExpandRockerBase64UrlString);
                }
            }
            else {
                ToastError("AjaxSetCollapseExpandImg", response.ErrorResults);
            }
        },
        error: function (xhr, status, error) {
            var errormsg = "Error " + xhr.status + ", " + xhr.statusText;
            ToastError("AjaxSetCollapseExpandImg", errormsg);
            console.log('AJAX: ' + errormsg);
        }
    });
}


function CollapseAllRecordIdsFromCookie() {

    var RecordIds = GetCollapsedRecordIdsCookieAsArray();
}

function FindCollapseExpandImageFromRow(RowObj) {

    if (!RowObj)
        return;
    if (RowObj.length == 0)
        return;

    var ImgElementObj = RowObj.find('.CollapseExpandImg');
    return ImgElementObj;
    
}

function FindGridRowFromElement(ElementObj) {

    if (!ElementObj)
        return;
    if (ElementObj.length == 0)
        return;

    // Already a row?
    if (ElementObj.is('tr'))
        return ElementObj;

    // Get up-parent tr
    return ElementObj.closest('tr');
}


/**
 * Search inside the row for a hiearchy level (for grouping records)
 * Find an element with a 'data-level' attribute
 * @param {any} RowObj
 */
function FindRowLevel(RowObj) {

    if (!RowObj)
        return -1;
    if (RowObj.length == 0)
        return -1;

    // Search inside the row for an element with a data-level attribute
    var ElementObj = RowObj.find(`[data-level]`);
    if (!ElementObj)
        return -1;
    if (ElementObj.length == 0)
        return -1;

    // Group records by level
    var nLevel = ElementObj.attr('data-level');
    return nLevel;
}

function IsRowCollapsed(RowObj) {

    if (!RowObj)
        return false;
    if (RowObj.length == 0)
        return false;

    var bCollapsed = RowObj.attr("data-collapsed");
    if (bCollapsed) bCollapsed = true; else bCollapsed = false;
    return bCollapsed;
}

function FindNextRowAtSameLevel(RowObj) {

    if (!RowObj)
        return;
    if (RowObj.length == 0)
        return;

    var nFindLevel = FindRowLevel(RowObj);
    if (nFindLevel < 0)
        return;

    // Loop over rows (after this one)
    var NextRow = RowObj.next('tr');
    while (NextRow && NextRow.length > 0) {

        // Break when the row level is <= to the begin level
        var nNextRowLevel = FindRowLevel(NextRow);

        // Same level!
        if (nNextRowLevel <= nFindLevel)
            return NextRow;

        // Get next row
        NextRow = NextRow.next('tr');
    }
}

function FindRecordIdByGridElement(ElementObj) {

    // Begin by finding the row this element is in
    if (!ElementObj)
        return;
    if (ElementObj.length == 0)
        return;
    var RowObj = FindGridRowFromElement(ElementObj);

    // Find an element with a GUID (this is the record id)
    var ElementObj = RowObj.find(`[data-guid]`);
    if (!ElementObj)
        return;
    if (ElementObj.length == 0)
        return;

    // Get the data-guid attribute
    var sRecordId = ElementObj.attr("data-guid");
    return sRecordId;
}

function FindGridRowByRecordId(GridObj, sRecordId) {

    // Search inside the grid for an element with a data-guid attribute
    var ElementObj = GridObj.find("[data-guid='" + sRecordId + "']");
    if (!ElementObj)
        return;
    if (ElementObj.length == 0)
        return;

    var RowObj = FindGridRowFromElement(ElementObj);
    return RowObj;
}

function CollapseRowById(GridObj, sRecordId) {

    var RowObj = FindGridRowByRecordId(GridObj, sRecordId);
    CollapseRow(RowObj);
}

function CollapseRow(RowObj) {

    if (!RowObj)
        return;
    if (RowObj.length == 0)
        return;

    // Collapse all rows with a higher level below this row
    var nBeginLevel = FindRowLevel(RowObj);
    if (nBeginLevel < 0)
        return;

    // Remember that this row is collapsed
    RowObj.attr("data-collapsed", "true");

    // Set new collapse/expand image
    var ImgElementObj = FindCollapseExpandImageFromRow(RowObj);
    AjaxSetCollapseExpandImg(ImgElementObj, false/*bCollapsed*/);

    // Loop over rows (after this one)
    var NextRow = RowObj.next('tr');
    while (NextRow && NextRow.length > 0) {

        // Only hide those with a higher level
        // Break when the row level is <= to the begin level
        var nNextRowLevel = FindRowLevel(NextRow);
        if (nNextRowLevel <= nBeginLevel)
            break;

        // Hide the row using "display: none"
        NextRow.hide();

        // Get next row
        NextRow = NextRow.next('tr');
    }
}


function ExpandRow(RowObj) {

    if (!RowObj)
        return;
    if (RowObj.length == 0)
        return;

    // Collapse all rows with a higher level below this row
    var nBeginLevel = FindRowLevel(RowObj);
    if (nBeginLevel < 0)
        return;

    // Forget "collapsed" attribute
    RowObj.removeAttr("data-collapsed");

    // Set new collapse/expand image
    var ImgElementObj = FindCollapseExpandImageFromRow(RowObj);
    AjaxSetCollapseExpandImg(ImgElementObj, true/*bCollapsed*/);

    // Loop over rows (after this one)
    var NextRow = RowObj.next('tr');
    while (NextRow && NextRow.length > 0) {

        // Only hide those with a higher level
        // Break when the row level is <= to the begin level
        var nNextRowLevel = FindRowLevel(NextRow);
        if (nNextRowLevel <= nBeginLevel)
            break;

        // Show the row using "display: block"
        NextRow.show();

        // If this next row is collapsed, then don't show any rows below it
        // Jump over all the rows below this one
        var bCollapsed = IsRowCollapsed(NextRow);
        if (bCollapsed) {
            NextRow = FindNextRowAtSameLevel(NextRow);
            continue;
        }

        // Get next row
        NextRow = NextRow.next('tr');
    }
}

function CollapseAllRows(ElementObj, bUpdateCollapsedCookie) {

    var RowsArray = GetAllGroupRows(ElementObj);
    if (!RowsArray)
        return;
    if (RowsArray.length == 0)
        return;

    for (i = 0; i < RowsArray.length; ++i) {
        var RowObj = RowsArray[i];
        var sRecordId = FindRecordIdByGridElement(RowObj);
        if (bUpdateCollapsedCookie)
            AddIdToCollapsedCookie(sRecordId);
        CollapseRow(RowObj);
    }
}

function ExpandAllRows(ElementObj, bUpdateCollapsedCookie) {

    var RowsArray = GetAllGroupRows(ElementObj);
    if (!RowsArray)
        return;
    if (RowsArray.length == 0)
        return;

    for (i = 0; i < RowsArray.length; ++i) {
        var RowObj = RowsArray[i];
        var sRecordId = FindRecordIdByGridElement(RowObj);
        if (bUpdateCollapsedCookie)
            RemoveIdFromCollapsedCookie(sRecordId);
        ExpandRow(RowObj);
    }
}


/**
 * Return an array of rows that have rocker buttons in them
 * Rocker buttons are found in group rows (for collapsing children)
 * @param {any} ElementObj
 */
function GetAllGroupRows(ElementObj) {

    if (!ElementObj)
        return;
    if (ElementObj.length == 0)
        return;

    var TableObj = ElementObj.closest('table');
    if (!TableObj)
        return;
    if (TableObj.length == 0)
        return;

    // Find all the rocker buttons
    var ImgElementObjs = TableObj.find('.CollapseExpandImg');
    if (!ImgElementObjs)
        return;
    if (ImgElementObjs.length == 0)
        return;

    // Convert all the rocker button images to the rows they are found in
    var RowsArray = [];
    for (i = 0; i < ImgElementObjs.length; ++i) {
        var ImgElementObj = $(ImgElementObjs[i]);
        var RowObj = FindGridRowFromElement(ImgElementObj);
        if (RowObj && RowObj.length > 0)
            RowsArray.push(RowObj);
    }

    // Return array of rows that have rocker buttons in them
    // Rocker buttons are found in group rows (for collapsing children)
    return RowsArray;
}


/**
 * User clicked on a rocker button to collapse/expand it
 * Example: Projects can be collapsed to hide time logs under it
 * Toggle the collapse/expand state
 * Look up the record in the cookies for collapse status
 * @param {any} element
 */
function OnToggleCollapseExpand(element) {

    // Img element with rocker button to collapse or expand groups
    // Example: Projects can be collapsed to hide time logs under it
    if (!element)
        return;

    var ImgElementObj = $(element);
    if (!ImgElementObj)
        return;
    if (ImgElementObj.length == 0)
        return;

    // The recordset guid is stored with the img element
    var sRecordId = ImgElementObj.attr('data-guid');
    var sDbColumn = ImgElementObj.attr('data-dbcolumn');
    var sDbTable = ImgElementObj.attr('data-dbtable');
    if (!sRecordId)
        return;
    if (!sDbTable)
        return;

    var RowObj = FindGridRowFromElement(ImgElementObj);
    if (!RowObj)
        return;
    if (RowObj.length == 0)
        return;

    var nLevel = FindRowLevel(RowObj);

    // Temporarily remove column fixing because of row height issues
    // First, find grid object for this row (ui-iggrid)
    ///////// Doesn't work ///////////////
    var GridId = $('input[id="HiddenSelectedGridId"]').val();
    var GridObject = $("#" + GridId);
    var bHasColumnFixing = HasAnyFixedColumns(GridObject);

    var bAltKeyDown = event.altKey;
    var bCollapsed = IsRowCollapsed(RowObj);
    if (bCollapsed) {
        RemoveIdFromCollapsedCookie(sRecordId);
        if (bAltKeyDown)
             ExpandAllRows(RowObj, true/*bUpdateCollapsedCookie*/);
        else ExpandRow(RowObj);
    }
    else {
        AddIdToCollapsedCookie(sRecordId);
        if (bAltKeyDown)
             CollapseAllRows(RowObj, true/*bUpdateCollapsedCookie*/);
        else CollapseRow(RowObj);
    }

    // Prevent the next popup because it gets in the way
    CloseGridEditPopupDialog();
    PreventNextGridEditPopup = new Date();

    // Must rebuild because Gantt link lines cannot be moved
    RebuildGanttLinkLinesFromSelectedContentPane();

    // Refresh to fix rows
    // This is necessary because rows in "fixed" and "unfixed" grid areas are now out of sync. Refreshing the entire view is the only remedy
    if (bHasColumnFixing)
        RefreshSelectedContent();
}


function OnGridHeaderClickEvent(GridElement, HeaderElement, event) {

    if (!GridElement)
        return;
    if (!HeaderElement)
        return;
    if (!event)
        return;
    if (!event.target)
        return;

    event.preventDefault();

    var GridId = GridElement.attr('id');
    //GridId = GridId.replace("_headers", "");


    var ClickedCellHtmlElement = event.target;
    var ClickedCellElement = $(ClickedCellHtmlElement);
    var ColumnObject = GetColumnObjectFromElement(ClickedCellElement);
    var ColumnIndex = -1;
    var ColumnKey = "";
    if (ColumnObject) {
        ColumnIndex = ColumnObject[0].cellIndex;
        ColumnKey = ColumnObject.attr("id");
        if (ColumnKey)
            ColumnKey = ColumnKey.replace(GridId + "_", "");
    }

    var ContentType = GetSelectedContentType();
    var LayoutString = LoadSelectedPaneLayoutStringFromCookie();

    //if (!LayoutString) {
    //    ToastError("Header click", "You cannot sort by this column");
    //    return;
    //}

    // Process header click event
    AjaxSortGridColumn(ContentType, LayoutString, ColumnKey, ColumnIndex);
}




function SetupGridHeaderContextMenuHandler(GridElement) {

    var HeaderId = GridElement.attr('id') + "_headers";
    var HeaderObject = $("#" + HeaderId);

    HeaderObject.contextmenu(function (evt) {

        // Call JavaScript context menu
        OnGridHeaderContextMenuEvent(GridElement, HeaderObject, evt);
    });
}


function SetupGridHeaderClickHandler(GridElement) {

    var HeaderId = GridElement.attr('id') + "_headers";
    var HeaderObject = $("#" + HeaderId);

    HeaderObject.click(function (evt) {

        // Call JavaScript context menu
        OnGridHeaderClickEvent(GridElement, HeaderObject, evt);
    });
}





//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
// Column fixing

/*
Event which is fired when column fixing operation is finished.

evt
Type: Event
JQuery event object.

args
Type: Object
columnIdentifier
Type: Object
Gets the columnKey or the columnIndex.

isGroupHeader
Type: Bool
Gets whether header cell has children(use this argument ONLY when multi-column-headers are enabled).

owner
Type: Object
Gets a reference to the widget.*/

function GridColumnFixed(evt, args) {

    // https://www.igniteui.com/help/api/2022.2/ui.iggridcolumnfixing#options:columnSettings
    var GridId = evt.target.id;

    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return;

    SaveColumnFixedToCookie(ContentType, args.columnIdentifier, true);

    //ToastError("GridColumnFixed: " + args.columnIdentifier);
}


function GridColumnUnfixed(evt, args) {

    // https://www.igniteui.com/help/api/2022.2/ui.iggridcolumnfixing#options:columnSettings
    var GridId = evt.target.id;

    var ContentType = GetSelectedContentType();
    if (!ContentType)
        return;

    SaveColumnFixedToCookie(ContentType, args.columnIdentifier, false);

//    ToastError("GridColumnUnfixed: " + args.columnIdentifier);
}





//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
//\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\////\\//
// Cell formatting
// See InfragisticsGridColumn.FormatterFunction

function FormatGridCellDoubleF2(val) {

    try {
        if (val === null)
            return val;

        var FormatterLocale = ThisLoginUserLocale;
        if (!FormatterLocale)
            FormatterLocale = "en-US";

        // val comes in as a string
        // Must be converted to a number before Intl.NumberFormat() can convert it to a localized string
        // Example: 2,561 --> 2,56
        var NumberVal = StringToNumberFromLocale(val, FormatterLocale);

        const formatter = new Intl.NumberFormat(FormatterLocale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
        var sFormattedValue = formatter.format(NumberVal);
        return sFormattedValue;
    }
    catch (e) {
        return val;
    }
}


function FormatGridCellDoubleC(val) {
    try {
        if (val === null)
            return val;

        var FormatterLocale = ThisLoginUserLocale;
        var FormatterCurrency = ThisLoginUserISO4217CurrencyCode;
        if (!FormatterLocale || !FormatterCurrency) {
            FormatterLocale = "en-US";
            FormatterCurrency = "USD";
        }

        // val comes in as a string
        // Must be converted to a number before Intl.NumberFormat() can convert it to a localized string
        // Example: 2,561 --> 2,56
        var NumberVal = StringToNumberFromLocale(val, FormatterLocale);

        const formatter = new Intl.NumberFormat(FormatterLocale, {
            style: 'currency',
            currency: FormatterCurrency, // 'USD'
        });

        var sFormattedValue = formatter.format(NumberVal);
        return sFormattedValue;
    }
    catch (e) {
        return val;
    }
}



function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
}

function StringToNumberFromLocale(val, FormatterLocale) {
    try {
        if (isNumeric(val))
            return val;

        if (!FormatterLocale)
            FormatterLocale = "en-US";

        // Nasty conversion from string to number
        // https://stackoverflow.com/questions/11665884/how-can-i-parse-a-string-with-a-comma-thousand-separator-to-a-number
        const example = Intl.NumberFormat(FormatterLocale).format('1.1');
        const cleanPattern = new RegExp(`[^-+0-9${example.charAt(1)}]`, 'g');
        const cleaned = val.replace(cleanPattern, '');
        const normalized = cleaned.replace(example.charAt(1), '.');
        var NumberVal = parseFloat(normalized);
        return NumberVal;
    }
    catch (e) {
        return val;
    }
}


function GetSelectedRecordsetObjectFromArguments(...arguments) {

    var SelectedRecordsetObject = {};
    if (arguments != null && Array.isArray(arguments) && arguments.length > 0) {

        var MenuCndObject = arguments[0];
        if (MenuCndObject != null && MenuCndObject.length > 0) {

            var menucmd = MenuCndObject[0];
            if (menucmd && menucmd.TargetRecordset) {

                SelectedRecordsetObject.SelectedDataType = menucmd.TargetRecordset.datatype;
                SelectedRecordsetObject.SelectedTableId = menucmd.TargetRecordset.tablename;
                SelectedRecordsetObject.SelectedRecordId = menucmd.TargetRecordset.recordid;
            }
        }
    }

    return SelectedRecordsetObject;
}


// Works, but upgraded to use locale
//function FormatGridCellDoubleF2(val) {

//    try {
//        if (val === null)
//            return val;
//        val = parseFloat(val);
//        if (isNaN(val))
//            return "0.00";

//        return val.toFixed(2);
//    }
//    catch (e) {
//        return val;
//    }
//}


// Works, but upgraded to use locale
//function FormatGridCellDoubleC(val) {
//    try {
//        if (val === null)
//            return val;

//        var CurrencySym = ThisLoginUserCurrencySym;
//        if (!CurrencySym)
//            CurrencySym = "$";

//        try { val = val.replace(CurrencySym, ""); } catch (e1) { }
//        val = parseFloat(val);
//        if (isNaN(val))
//            return CurrencySym + "0.00";
//        return CurrencySym + val.toFixed(2);
//    }
//    catch (e) {
//        return val;
//    }
//}




