function browse_grid(opt) {

if ( typeof opt === 'string' ) {
    // can pass single string arg for table
    opt = {table: opt};
}

// opt.table = table name
// opt.colModel = column data
if (!opt.tableId) opt.tableId = 'jqg-table';
if (!opt.navId) opt.navId = opt.tableId + '-nav';
if (!opt.rows) opt.rows = 5;
if (!opt.baseUrl) {
    opt.baseUrl = '/class/' + opt.table + '_class.php?_m=';
}
if (!opt.colModel) {
    // get colModel
    jQuery.ajax( {
        url: opt.baseUrl + 'jqg_colmodel_json',
        dataType: 'json',
        // Needs to be synchronous so we don't proceed until colModel is set:
        async: false,
        success: function (data) {
            opt.colModel = data;
        }
    } );
}

var tableJQ = jQuery('#' + opt.tableId);

var isSearchResults = window.location.search != '' &&
    jQuery('#' + opt.table + '_form .searchbutton').length > 0; 

if ( opt.setColsId && jQuery('#' + opt.setColsId).length == 0 ) {
    // Add button if missing
    jQuery('<button>Set Columns</button>').attr('id', opt.setColsId)
        .insertAfter('#' + opt.tableId);
}

if ( opt.navId && jQuery('#' + opt.navId).length == 0 ) {
    // Add div if missing
    jQuery('<div></div>').attr('id', opt.navId)
        .insertAfter('#' + opt.tableId);
}

if ( !opt.onSelectRow ) {
    onSelectRow = function (rowid) {
        console.log("onSelectRow " + rowid);
    };
}    

if ( opt.searchId && jQuery('#' + opt.searchId).length == 0 ) {
    // Add div if missing
    jQuery('<div></div>').attr('id', opt.searchId)
        .insertBefore('#' + opt.tableId);
}

jQuery('#' + opt.table + '_form .searchbutton').click(
    function () {
        var formData = jQuery(this.form).serialize();
        console.log(formData);
        jQuery.getJSON(
            opt.baseUrl + 'jqg_json',
            formData,
            function (data) {
                // Why can't addJSONData be called directly from
                // the jQuery object?
                tableJQ[0].addJSONData(data);
            }
        );
        return false;
    }
);

var fixEditForm =  function (f) {
    console.log("afterShowForm");
    var cm = tableJQ.getGridParam('colModel');
    for ( var i = 0; i < cm.length; i++ ) {
        if (!cm[i].custom || !cm[i].custom.help) continue;
        jQuery('#' + cm[i].name).attr('title', cm[i].custom.help);
    }
    //jQuery('#TblGrid_' + opt.tableId).width('98%');
    jQuery('#editmod' + opt.tableId)
        .width( jQuery('#TblGrid_' + opt.tableId).width() + 40 )
        .height( jQuery('#edithd' + opt.tableId).height() +
            jQuery('#editcnt' + opt.tableId).height() + 20 );
    jQuery(document).keypress( // set to close on ESC
        function (e) {
            var keyCode = e.keyCode || e.which;
            if (keyCode == 27) { // ESC
                jQuery('#editmod' + opt.tableId).jqmHide();
                return false;
            }
        }
    );
}

tableJQ.jqGrid( {

    onSelectRow: opt.onSelectRow,

    onPaging: function (pgButton) {
        console.log( "Paging " + pgButton + " to page " +
            tableJQ.getGridParam("page") );
    },

    ondblClickRow: function (rowId) {
        tableJQ.editGridRow(rowId, {afterShowForm: fixEditForm});
    },

    afterInsertRow: function (rowId, rowData, rowElem) {
        var cm = tableJQ.getGridParam('colModel');
        var formatted;
        for ( var i = 0; i < cm.length; i++ ) {
            formatted = false;
            if (cm[i].edittype == 'select') {
                formatted = cm[i].editoptions.value[rowData[cm[i].name]];
            }
            else if (cm[i].custom && cm[i].custom.sprintf_format) {
                var sprintfArgs = [cm[i].custom.sprintf_format];
                for ( var j = 0; j < cm[i].custom.sprintf_values.length; j++ ) {
                    var name = cm[i].custom.sprintf_values[j];
                    sprintfArgs[j + 1] = rowData[name];
                }
                // The apply() method makes it possible to call a function by
                // passing an array of arguments rather than a list of
                // individual arguments.  The first argument to apply() is an
                // object so that you can call the function as a method of that
                // object.  In this case, we don't need that, so it's null. 
                formatted = sprintf.apply(null, sprintfArgs);
            }
            if ( formatted !== false ) {
                tableJQ.setCell(rowId, cm[i].name, formatted);
            }
        }
        if (opt.afterInsertRow) {
            opt.afterInsertRow(rowId, rowData, rowElem);
        }
    },

    datatype: function (postdata) {
        if (isSearchResults) {
            isSearchResults = false;
            jQuery('#' + opt.table + '_form .searchbutton').click();
            return;
        }
        // console.log(postdata);
        if (jQuery('#use_cache').attr('checked')) {
            console.log("Use Cache!");
            if (tableJQ[0].addJSONData(jqgrid_json(opt.table, postdata))) {
                console.log("Cached Data added okay.");
            }
            else {
                console.log("Error adding Gears SQLITE data.");
            }
        }
        else {
            // console.log("Use Live Data!");
            // use $.getJSON() for this ?
            jQuery.ajax({
                url: opt.baseUrl + 'jqg_json',
                data: postdata,
                dataType: 'json',
                complete: function (jsondata,stat) {
                    if (stat == "success") {
                        var myjsondata = eval("("+jsondata.responseText+")"); 
                        // console.log(myjsondata);
                        // the docs are wrong, the load status isn't returned
                        try {
                          tableJQ[0].addJSONData(myjsondata);
                          var loadComplete =
                            tableJQ.getGridParam('loadComplete');
                          if (jQuery.isFunction(loadComplete)) {
                            loadComplete();
                          }
                        } catch(e) { 
                          console.log("Error adding data: " + e);
                        }
                    }
                }
            });
        }
    },

    gridComplete: opt.gridComplete,
    loadComplete: opt.loadComplete,
	rowNum: opt.rows,
	rowList: [5,10,25,50,100,250,500],
	viewrecords: true,
    //shrinkToFit: false,
	height: "100%",
	imgpath: "/common/js/grid/themes/sand/images",
	pager: jQuery('#' + opt.navId),
	jsonReader: {cell: '', id: 0},
	editurl: opt.baseUrl + 'jqg_edit',
    colModel: opt.colModel
} ).navGrid('#' + opt.navId,
    {edit: true, add: true, del: true},
    {afterShowForm: fixEditForm},
    {afterShowForm: fixEditForm}
);

// Add search above columns if searchId set
if (opt.searchId) {
    jQuery('#' + opt.searchId).filterGrid('#' + opt.tableId,
        {gridModel: true, gridToolbar: true});
}

// Add click handler for "Set Columns" button if setColsID set
if (opt.setColsId) {
    jQuery('#' + opt.setColsId).click( function () {
        jQuery('#' + opt.tableId).setColumns(
            {modal: true, width: 400, height: 600, left: 100, top: 100}
        );
    } );
}

}

function detailViewFormatter(el, cellval, opts) {
    // urlTemplate or contentTemplate must be set.  Both can contain keys
    // surrounded by {} that are replaced by values from the row data 
    // (though currently only the special keys 'id' and 'cellval' work),
    // so something like:
    // http://example.com/path/script.php?site_id={id}
    //
    // target is a jQuery selector, normally an id preceded by '#'.  It
    // must be set if contentTemplate is set, in which case the HTML
    // defined by contentTemplate ill be loaded into the element 
    // (normally a div) defined by the target.  If contentTemplate is
    // not set but urlTemplate is, then the content will be retrieved 
    // from the URL.

    function fillTemplate(template, values) {
        return template.replace(
            /\{(\w+)\}/g,
            function (match, key) { return values[key]; }
        );
    }

    var clickFn;
    var values = {id: opts.rowId, cellval: cellval};
    var target = opts.colModel.formatoptions.target;
    if (opts.colModel.formatoptions.urlTemplate) {
        var url = fillTemplate(opts.colModel.formatoptions.urlTemplate, values);
        if (target) {
            clickFn = function () {
                jQuery(target).show().load(url);
                return false;
            };
        }
    }
    if (opts.colModel.formatoptions.contentTemplate) {
        var content = fillTemplate(opts.colModel.formatoptions.contentTemplate,
             values);
        clickFn = function () {
            jQuery(target).show().html(content);
            return false;
        };
    }

    var link = jQuery('<a></a>').html(cellval);
    if (url) link.attr('href', url);
    if (clickFn) link.click(clickFn);
    $(el).html(link);

}

