Friday, February 25, 2011

jqGrid - customizing the multi-select option (restrict single selection and adding custom events)

Goal:

Using the jgGrid to enable a selection of a checkbox for row selection - which is easy to set in the jqGrid - but also only allowing a single row to be selectable at a time while adding events based on whether the row was selected or de-selected.

Environment:

Issue:

The jqGrid does not support the option to restrict the multi-select to only allow for a single selection. You may ask, why bother with the multi-select checkbox function if you only want to allow for the selection of a single row? Good question, as an example, you want to reserve the selection of a row to trigger another kind of event and use the checkbox multi-select to handle a different kind of event; in other words, when I select the row I want something entirely different to happen than when I select to check off the checkbox for that row.
Also the setSelection method of the jqGrid is a toggle and has no support for determining whether the checkbox has already been selected or not, So it will simply act as a switch - which it is designed to do - but with no way out of the box to only check off the box (as in not to de-select) rather than act like a switch.
Furthermore, the getGridParam('selrow') does not indicate if the row was selected or de-selected, which seems a bit strange and is the main reason for this blog post.

Solution:

How this will act:

When you check off a multi-select checkbox in the gird, and then commence to select another row by checking off that row's multi-select checkbox - I'm not talking there about clicking on the row but using the grid's multi-select checkbox - it will de-select the previous selection so that you are always left with only a single selection.
Furthermore, once you select or de-select a multi-select checkbox, fire off an event that will be determined by whether or not the row was selected or de-selected, not just merely clicked on. So if I de-select the row do one thing but when selecting it do another.

Implementation (this of course is only a partial code snippet):

            multiselect: true,
            multiboxonly: true,
            onSelectRow: function (rowId) {
                var gridSelRow = $(item).getGridParam('selrow');
                var s;
                s = $(item).getGridParam('selarrrow');
                if (!s || !s[0]) {
                    $(item).resetSelection();
                    $('#productLineDetails').fadeOut();
                    lastsel = null;
                    return;
                }
                var selected = $.inArray(rowId, s) != -1;
                if (selected) {
                    $('#productLineDetails').show();
                }
                else {
                    $('#productLineDetails').fadeOut();
                }

                if (rowId && rowId !== lastsel && selected) {
                    $(item).GridToForm(gridSelRow, '#productLineDetails');
                    if (lastsel) $(item).setSelection(lastsel, false);
                }
                lastsel = rowId;
            },

In the example code above:

The "item" property is the id of the jqGrid.
The following to settings ensure that the jqGrid will add the new column to select rows with a checkbox and also the not allow for the selection by clicking on the row but to force the user to have to click on the multi-select checkbox to select the row:
multiselect: true,

multiboxonly: true,
Unfortunately the var gridSelRow = $(item).getGridParam('selrow') function will only return the row the user clicked on or rather that the row's checkbox was clicked on and NOT whether or not it was selected nor de-selected, but it retrieves the row id, which is what we will need.
The following piece get's all rows that have been selected so far, as in have a checked off multi-select checkbox:
var s;

s = $(item).getGridParam('selarrrow');
Now determine if the checkbox the user just clicked on was selected or de-selected:
var selected = $.inArray(rowId, s) != -1;
If it was selected then show a container "#productLineDetails", if not hide that container away.
The following instruction populates a form with the grid data using the built-in GridToForm method (just mentioned here as an example) ONLY if the row has been selected and NOT de-selected but more importantly to de-select any other multi-select checkbox that may have been selected:
if (rowId && rowId !== lastsel && selected) {
                    $(item).GridToForm(gridSelRow, '#productLineDetails');
                    if (lastsel) $(item).setSelection(lastsel, false);
}

Thursday, February 17, 2011

Menu widget - no jQuery nor Javascript required - pure CSS

Goal:

Create a menu widget that does not require any javascript, extremely lightweight, very fast, soley based on CSS, compatible with FireFox and Chrome.
Issues:

May have some rendering issues in some versions of IE, sorry :-)
Instruments:

* css file
* html with specific menu format
* jQuery-ui library - optional if you want to use your own images/colors

Implementation Details:
HTML:

CSS:

/* =Menu
----------------------------------------------------------------------------------------- */
#header #header_Menubar
{
margin: 0;
padding: 0;
border: 0;
width: 100%;
height: 22px;
}

#header
{
background-color: #99cccc;
background-color: #aaccee;
background-color: #5BA3E0;
background-color: #006cb1;

}

/* Set menu bar background color */
#header #header_Menubar
{
background-attachment: scroll;
background-position: left center;
background-repeat: repeat-x;
}

/* Set main (horizontal) menu typology */
#header .linkList0
{
padding: 0 0 1em 0;
margin-bottom: 1em;
font-family: 'Trebuchet MS', 'Lucida Grande',
Verdana, Lucida, Geneva, Helvetica,
Arial, sans-serif;
font-weight: bold;
font-size: 1.085em;
font-size: 1em;
}

/* Set all ul properties */
#header .linkList0, #header .linkList0 ul
{
list-style: none;
margin: 0;
padding: 0;
list-style-position: outside;
}

/* Set all li properties */
#header .linkList0 > li
{
float: left;
position: relative;
font-size: 90%;
margin: 0 0 -1px;
width: 9.7em;
padding-right: 2em;
z-index: 100; /*IE7: Fix for IE7 hiding drop down list behind some other page elements */
}

/* Set all li properties */
#header .linkList01 > li
{
width: 190px;
}

#header .linkList0 .linkList01 li
{
margin-left: 0px;
}

/* Set all list background image properties */
/*#header .linkList0 li a
{
background-position: left center;
background-image: url( '../Content/Images/VerticalButtonBarGradientFade.png' );
background-repeat: repeat-x;
background-attachment: scroll;
}*/
/* Set all A ancor properties */
#header .linkList0 li a
{
display: block;
text-decoration: none;
line-height: 22px;
}

/* IE7: Fix for a bug in IE7 where the margins between list items is doubled - need to set height explicitly */
*+html #header .linkList0 ul li
{
height: auto;
margin-bottom: -.3em;
}

/* Menu: Set different borders for different nested level lists
-------------------------------------------------------------- */
#header .linkList0 > li a
{
border-left: 10px solid Transparent;
border-right: none;
}

#header .linkList0 > li a
{
border-left: 0px;
margin-left: 0px;
border-right: none;
}

#header .linkList0 .linkList01 > li a
{
border-left: 8px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

#header .linkList0 .linkList01 .linkList001 > li a
{
border-left: 6px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

#header .linkList0 .linkList01 .linkList001 .linkList0001 > li a
{
border-left: 4px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

/* Link and Visited pseudo-class settings for all lists (ul) */
#header .linkList0 a:link, #header .linkList0 a:visited
{
display: block;
text-decoration: none;
padding-left: 1em;
}

/* Hide all the nested/sub menu items */
#header .linkList0 ul
{
display: none;
padding: 0;
position: absolute; /*Important: must not impede on other page elements when drop down opens up */
}

/* Hide all detail popups */
#header .detailPopup
{
display: none;
}

/* Set the typology of all sub-menu list items li */
/*#header .linkList0 ul li
{
background-color: #AACCEE;
background-position: left center;
background-image: url( '../Content/Images/VerticalButtonBarGradientFade.png' );
background-repeat: repeat-x;
background-attachment: scroll;
}*/

#header .linkList0 ul li.more
{
background: Transparent url('../Content/Images/ArrowRight.gif') no-repeat right center;
}

/* Header list's margin and padding for all list items */
#header .linkList0 ul li
{
margin: 0 0 0 1em;
padding: 0;
}

#header .linkList01 ul li
{
margin: 0;
padding: 0;
width: 189px;
}

/* Set margins for the third li sibling (Plan a Call) to display to the right of the parent menu
to avoid the sub-menu overlaying the menu items below */
#header .linkList0 li.more .linkList01 li.more > ul.linkList001
{
margin: -1.7em 0 0 13.2em; /*Important, must be careful, if tbe EM since gap increases too much bewteen nested lists the gap will make the nested-list collapse prematurely */
}

/* Set right hand arrow for list items with sub-menus (class-more) */
#header li.more
{
background: Transparent url('../Content/Images/ArrowRight.gif') no-repeat right center;
padding-right: 48px;
}

/* Menu: Dynamic Behavior of menu items (hover, visted, etc)
----------------------------------------------------------- */
#header .linkList0 li a:link, #header .linkList01 li a:link
{
display: block;
}

#header .linkList0 li a:visited, #header .linkList01 li a:visited
{
display: block;
}

#header .linkList0 > li:hover
{
}

#header .linkList01 > li:hover a
,#header .linkList001 > li:hover a
{
text-decoration: underline;
}

#header .linkList0 > li abbr:hover span.detailPopup
{
display: block;
position: absolute;
top: 1em;
left: 17em;
border: double 1px #696969;
border-style: outset;
width: 120%;
height: auto;
padding: 5px;
font-weight: 100;
}

#header .linkList0 > li:hover
,#header .linkList0 .linkList01 > li:hover
{
}

#header .linkList0 .linkList01 .linkList001 > li:hover
{
}

#header .linkList0 .linkList01 .linkList001 .linkList0001 > li:hover
{
}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 li:hover > ul
{
display: block;
}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 .linkList01 li:hover > ul
{
display: block;
background: -moz-linear-gradient(top, #1E83CC, #619FCD);
/* Chrome, Safari:*/
background: -webkit-gradient(linear,
center top, center bottom, from(#1E83CC), to(#619FCD));

}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 .linkList01 .linkList001 li:hover > ul
{
display: block;
}

/* Set right hand arrow for list items with sub-menus (class-more) on hover */
#header li.more:hover
{
}

Also some CSS for global settings that will affect this menu, you of course will have some other styling, but included it here so you can see how/why some css properties were set here:

/* Neutralize styling:
Elements we want to clean out entirely: */
html, body
{
margin: 0;
padding: 0;
font: 62.5%/120% Verdana, Arial, Helvetica, sans-serif;
}

/* Neutralize styling:
Elements with a vertical margin: */
h1, h2, h3, h4, h5, h6, p, pre,
blockquote, ul, ol, dl, address {
margin: 0; /* most browsers set some default value that is not shared by all browsers */
padding: 0; /* some borowsers default padding, set to 0 for all */
}

/* Apply left margin:
Only to the few elements that need it: */
li, dd, blockquote {
margin-left: 1em;
}

Thursday, February 10, 2011

jqgrid - dynamically load different drop down values for different rows depending on another column value

Goal:

As we all know the jqGrid examples in the demo and the Wiki always refer to static values for drop down boxes. This of course is a personal preference but in dynamic design these values should be populated from the database/xml file, etc, ideally JSON formatted.
Can you do this in jqGrid, yes, but with some custom coding which we will briefly show below (refer to some of my other blog entries for a more detailed discussion on this topic).
What you CANNOT do in jqGrid, referring here up and to version 3.8.x, is to load different drop down values for different rows in the jqGrid. Well, not without some trickery, which is what this discussion is about.

Issue:

Of course the issue is that jqGrid has been designed for high performance and thus I have no issue with them loading a  reference to a single drop down values list for every column. This way if you have 500 rows or one, each row only refers to a single list for that particular column. Nice!
SO how easy would it be to simply traverse the grid once loaded on gridComplete or loadComplete and simply load the select tag's options from scratch, via Ajax, from memory variable, hard coded etc? Impossible! Since their is no embedded SELECT tag within each cell containing the drop down values (remember it only has a reference to that list in memory), all you will see when you inspect the cell prior to clicking on it, or even before and on beforeEditCell, is an empty .
When trying to load that list via a click event on that cell will temporarily load the list but jqGrid's last internal callback event will remove it and replace it with the old one, and you are back to square one.

Solution:

Yes, after spending a few hours on this found a solution to the problem that does not require any updates to jqGrid source code, thank GOD!
Before we get into the coding details, the solution here can of course be customized to suite your specific needs, this one loads the entire drop down list that would be needed across all rows once into global variable. I then parse this object that contains all the properties I need to filter the rows depending on which ones I want the user to see based off of another cell value in that row. This only happens when clicking the cell, so no performance penalty. You may of course to load it via Ajax when the user clicks the cell, but I found it more efficient to load the entire list as part of jqGrid's normal editoptions: { multiple: false, value: listingStatus } colModel options which again keeps only a reference to the single list, no duplication.
Lets get into the meat and potatoes of it.
        var acctId = $('#Id').val();

        var data = $.Ajax({ url: $('#ajaxGetAllMaterialsTrackingLookupDataUrl').val(), data: { accountId: acctId }, dataType: 'json', async: false, success: function(data, result) { if (!result) alert('Failure to retrieve the Alert related lookup data.'); } }).responseText;
        var lookupData = eval('(' + data + ')');

        var listingCategory = lookupData.ListingCategory;

        var catList = '{';
        $(lookupData.ListingCategory).each(function() {
            catList += this.Id + ':"' + this.Name + '",';
        });
        catList += '}';



        var lastsel;
        var ignoreAlert = true;
        $(item)
        .jqGrid({
            url: listURL,
            postData: '',
            datatype: "local",
            colNames: ['Id', 'Name', 'Commission
Rep', 'Business
Group', 'Order
Date', 'Edit', 'TBD', 'Month', 'Year', 'Week', 'Product', 'Product
Type', 'Online/
Magazine', 'Materials', 'Special
Placement', 'Logo', 'Image', 'Text', 'Contact
Info', 'Everthing
In', 'Category', 'Status'],
            colModel: [
                { name: 'Id', index: 'Id', hidden: true, hidedlg: true },
                { name: 'AccountName', index: 'AccountName', align: "left", resizable: true, search: true, width: 100 },
                { name: 'OnlineName', index: 'OnlineName', align: 'left', sortable: false, width: 80 },
                { name: 'ListingCategoryName', index: 'ListingCategoryName', width: 85, editable: true, hidden: false, edittype: "select", editoptions: { multiple: false, value: eval('(' + catList + ')') }, editrules: { required: false }, formatoptions: { disabled: false} }

            ],
            jsonReader: {
                root: "List",
                page: "CurrentPage",
                total: "TotalPages",
                records: "TotalRecords",
                userdata: "Errors",
                repeatitems: false,
                id: "0"
            },
            rowNum: $rows,
            rowList: [10, 20, 50, 200, 500, 1000, 2000],
            imgpath: jQueryImageRoot,
            pager: $(item + 'Pager'),
            shrinkToFit: true,
            width: 1455,
            recordtext: 'Traffic lines',
            sortname: 'OrderDate',
            viewrecords: true,
            sortorder: "asc",
            altRows: true,
            cellEdit: true,
            cellsubmit: "remote",
            cellurl: editURL + '?rows=' + $rows + '&page=1',
            loadComplete: function() {

            },
            gridComplete: function() {

            },
            loadError: function(xhr, st, err) {

            },
            afterEditCell: function(rowid, cellname, value, iRow, iCol) {
                var select = $(item).find('td.edit-cell select');
                $(item).find('td.edit-cell select option').each(function() {
                    var option = $(this);
                    var optionId = $(this).val();
                    $(lookupData.ListingCategory).each(function() {
                        if (this.Id == optionId) {                          
                            if (this.OnlineName != $(item).getCell(rowid, 'OnlineName')) {
                                option.remove();
                                return false;
                            }
                        }
                    });
                });
            },

            search: true,
            searchdata: {},
            caption: "List of all Traffic lines",
            editurl: editURL + '?rows=' + $rows + '&page=1',
            hiddengrid: hideGrid


Here is the JSON data returned via the Ajax call during the jqGrid function call above (NOTE it must be { async: false}:
{"ListingCategory":[{"Id":29,"Name":"Document Imaging","OnlineName":"RF Globalnet"}   
,{"Id":1,"Name":"Ancillary Department","OnlineName":"Healthcare Technology Online"} 
,{"Id":2,"Name":"Asset Tracking","OnlineName":"Healthcare Technology Online"}   
,{"Id":3,"Name":"Asset Tracking","OnlineName":"RF Globalnet"}   
,{"Id":4,"Name":"Asset Tracking","OnlineName":"ISMR"}   
,{"Id":5,"Name":"Document Imaging","OnlineName":"Healthcare Technology Online"} 
,{"Id":6,"Name":"Document Imaging","OnlineName":"RF Globalnet"}   
,{"Id":7,"Name":"EMR/EHR Software","OnlineName":"Healthcare Technology Online"}]}
I only need the Id and Name for the drop down list, but the third column in the JSON object is important, it is the only that I match up with the OnlineName in the jqGrid column, and then in the loop during afterEditCell simply remove the ones I don't want the user to see. That's it!