Showing posts with label jquery. Show all posts
Showing posts with label jquery. Show all posts

Thursday, September 27, 2012

jQuery Plugin to Register a Function to Run on UpdatePanel Request

Short post, long title. Here is a simple jQuery plugin that makes it a little easier to add an begin/end request handler to the PageRequestManager on an ASP.NET web form.
(function ($, undefined) {
    $.registerAsyncBegin = function(callback) {
        var prm = Sys.WebForms.PageRequestManager.getInstance();
        if (prm) {
            prm.add_beginRequest(callback);
        }
        return callback;
    };
    $.registerAsyncEnd = function(callback) {
        var prm = Sys.WebForms.PageRequestManager.getInstance();
        if (prm) {
            prm.add_endRequest(callback);
        }
        return callback;
    };
})(jQuery);
This example adds a function to be executed after ever Update Panel request and runs the function. This is useful if you have a function that needs to do some initial processing as well as after a request.
($.registerAsyncEnd(SomeCallbackFunction))();

Thursday, September 6, 2012

Javascript Fix Up For Internet Explorer Select Options Truncation

OK, that is a long title, but the issue has real world implications. Since there are still requirements that require support for Internet Explorer 8, it would behoove me to have an elegant pattern that fixes the limitations of IE8 and enhances the user experience.

My initial pass is adapted from http://css-tricks.com/select-cuts-off-options-in-ie-fix/. I found it some what long winded and only solved part of the problem.
function dropDownListFixup(sender, args) {
    var el,
        ua = $.browser,
        up = (sender) ? "#" + sender._updatePanelClientIDs.join(",#") : null,
        selects = $("select", up);
    if (ua.msie && Number(ua.version) < 9) {
        selects
            .each(function () {
                el = $(this);
                el.data("oWidth", el.outerWidth());
            })
            .mouseenter(function () {
                $(this).css("width", "auto");
            })
            .bind("blur change", function () {
                el = $(this);
                el.css("width", el.data("oWidth"));
            });
    }
    selects.each(function (e) {
        this.title = $(":selected", $(this)).text();
    });
}

$(function () {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    prm.add_endRequest(dropDownListFixup);
    dropDownListFixup();
    $("body").delegate("select", "change", function (e) { 
        this.title = $(":selected", $(this)).text();
    });
});
After some refactoring and extending, I came up with a solution that is slightly more compact, but provides slightly more functionality and handles drop down lists in update panels. Since even if the drop downs are in modern browsers, the text may be truncated on the closed display if the width is set too small. To fix this issue, I set the title on the drop down so that if the user hovers over the control, the tool tip will display with the value.
// Adapted from: http://css-tricks.com/select-cuts-off-options-in-ie-fix/
function dropDownListSetTitles(sender, args, e) {
    var selects = $("select", ((sender) ? "#" + sender._updatePanelClientIDs.join(",#") : null));
    selects.each(function (e) {
        var el = $(this);
        el.data("oWidth", el.outerWidth());
        setDropDownListTitle.apply(this, [e]);
    });
}
function setDropDownListTitle(e) {
    this.title = $(":selected", $(this)).text();
}
$(function () { // dropDownListFixup
    dropDownListSetTitles();
    if ($.browser.msie && Number($.browser.version) < 9) { //// mouseenter  mouseleave
        $("body").delegate("select", "focus", function () {
            $(this).css("width", "auto");
        })
        .delegate("select", "blur change", function () {
            var el = $(this);
            el.css("width", el.data("oWidth"));
        });
    }
    $("body").delegate("select", "change", setDropDownListTitle);
    Sys.WebForms.PageRequestManager.getInstance().add_endRequest(dropDownListSetTitles);
});

Saturday, July 14, 2012

Prevent Text Box Selection And Automatically Select Different Control

I ran into an interesting issue where I needed to allow a user to select a contact in a custom SharePoint 2010 application. Preventing the selection of the text box is not as straight forward as one might hope, especially handling cross browser. Since SharePoint has the concept of a People Picker, I decided that I could leverage that pattern to increase application performance. The requirement had several parts:
  1. The result needed to only display the users name
  2. The ID of the contact needed to be saved
  3. The label needs to be able to send the value back to the server, but prevent people from editing the label
The first and third points are solved by using a text box instead of a label. The ID is saved by hiding ID text box so that it is posted back with the rest of the data. We make the solution slightly more elegant by treating the text box like modern browsers handle the file input control; clicking the text box opens the dialog. There is nothing that is specific to ASP.NET and can be easily converted to standard HTML.
<asp:TextBox ID="txtContact" onfocus="focusCtrl('btnContact', event);" runat="server"></asp:TextBox>
<asp:TextBox ID="txtContactID" style="display: none;" runat="server"></asp:TextBox>
<asp:Button ID="btnContact" OnClientClick="return contactPicker('txtContactID', 'txtContact');" Text="Select" CausesValidation="false" runat="server"></asp:Button>
<asp:RequiredFieldValidator ID="rfvContact" ControlToValidate="txtContact" ErrorMessage="Contact is required" ValidationGroup="Required" Display="Dynamic" InitialValue="" CssClass="ms-error" EnableClientScript="false" runat="server">
    <img alt="Validation Error" title="Contact is required" src="/_layouts/images/EXCLAIM.GIF" />
</asp:RequiredFieldValidator>
<script type="text/javascript">
     $(function () { focusCtrl('btnContact'); });
</script>
A couple important notes about the focusCtrl function, the code above assumes that the specified control will be initially selected, but the same function is used for the text box focus event with different outcomes. The initial run without the event won't pass an event into the function which won't trigger the control click event. Once the focus event has fired off an event is passed in and causes the controls click event to fire. I included a version of the contact picker function to show how we are populating controls on the callback.
function focusCtrl(ctlID, e) {
    var ctl = $('input[id$=\'' + ctlID + '\']');
    e = e || window.event;
    var t = (e) ? e.target || e.srcElement : null;
    if (ctl.length > 0) {
        ctl.focus();
        if (t != null) {
            ctl.click();
        }
    } else if (e || window.event) {
        $(t).blur();
        //Focus next textbox
        //$(t).next("input:not(input[type='submit']):visible").focus();
    }
    return false;
}

function contactPicker(txtID, txtLabel) {
    var options = {
        url: '../Contacts/ContactPicker.aspx',
        title: 'Contact Picker',
        allowMaximize: true,
        showClose: true,
        showMaximized: false,
        dialogReturnValueCallback: function (dialogResult, returnValue) {
                                        if (dialogResult == SP.UI.DialogResult.OK) {
                                            var ret = unescape(returnValue),
                                                name = ret.split(";#")[1];
                                            $("input[id$='" + txtName + "']").val(name);
                                            $("input[id$='" + txtID + "']").val(ret);
                                        }
                                    }
    };
    SP.UI.ModalDialog.showModalDialog(options);
    return false;
}

Wednesday, June 27, 2012

Google Top Bar Rearrange UserScript for GreaseMonkey

I ran across a script on UserScripts.org called Google topbar. It doesn’t appear to be maintained any longer and doesn’t worked in FireFox 13 (not sure if it didn't work before, but that is what I tested on). So I decided that I would use it as a base for an updated script to make it display the links I most commonly use. 

// ==UserScript==
// @name          Google Top Bar Rearrange
// @namespace     com.blogspot.intellectualponderings.GoogleToolBarRearrange
// @description   Custom Google Links and Order, based on http://userscripts.org/scripts/show/18128
// @include       http://*.google.*/*
// @include       https://*.google.*/*
// ==/UserScript==

(function(undefined) {
  var shared = {
    more: ['Maps', 'Images', 'Play', 'YouTube', 'News', 'Shopping'],
    top: ['Blogger', 'Calendar', 'Reader'],
    topBarList: "//ol[@class='gbtc']",
    topBarNode: "//li[@class='gbt'][a[span[text()='{{0}}']]]",
    moreList: "//ol[@id='gbmm']",
    moreNode: "//li[@class='gbmtc'][a[text()='{{0}}']|text()='{{0}}']",
    nodeLink: "//a"
  };
  
  var node = function(path, text, context) {
    path = (text) ? path.replace(/\{\{0\}\}/g, text) : path;
    context = context || document;
    return document.evaluate(path, context, null, XPathResult.ANY_TYPE,null).iterateNext();
  },
  toMoreMenu = function(text) {
    var tag = node(shared.topBarNode, text);
    if (tag) {
      var more = node(shared.moreList);
      if (more) {
          more.appendChild(tag);
          tag.className = 'gbmtc';
      }
    }
  },
  toTopMenu = function(text) {
    var more = node(shared.moreList),
        tag = node(shared.moreNode, text, more),
        alink, moreli;
    if (tag) {
      var topBar = node(shared.topBarList);
      if (topBar) {
        moreli = topBar.lastChild;
        topBar.appendChild(tag);
        tag.className = "gbt";
        alink = node(shared.nodeLink);
        alink.className = "gbzt";
        if (moreli) {
          topBar.removeChild(moreli);
          topBar.appendChild(moreli);
        }
      }
    }
  };

  if (window.top == window.self) {
      var i;
      for (i = 0; i < shared.more.length; i++) {
        toMoreMenu(shared.more[i]);
      }
      for (i = 0; i < shared.top.length; i++) {
        toTopMenu(shared.top[i]);
      }
  }
})();

And then I decided to change it so that it would use jQuery and be a bit more complete. In fact, I didn't keep much.

// ==UserScript==
// @name          Google Top Bar Rearrange
// @namespace     com.blogspot.intellectualponderings.GoogleToolBarRearrange
// @description   Custom Google Links and Order, based on http://userscripts.org/scripts/show/18128
// @include       http://*.google.*/*
// @include       https://*.google.*/*
// @grant         GM_getValue
// @grant         GM_setValue
// @require       https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js
// ==/UserScript==


(function(undefined) {
  var context = $("#gbz ol.gbtc"),
      more = ['Maps', 'Images', 'Play', 'YouTube', 'News', 'Shopping'],
      top = ['Blogger', 'Calendar', 'Reader'];
      
  var addItem = function(url, text) {
    var item = $("<li />").addClass("gbt").append(
                  $("<a />").attr("href", url).addClass("gbzt").attr("target", "_blank").append(
                    $("<span/>").addClass("gbtb2")
                  ).append(
                      $("<span />").addClass("gbts").html(text)
                  )
                );
        $(context).find("li.gbt:last-child").before(item);
    },
    removeItem = function(text) {
    var link = $(context).find("li.gbt span:contains('" + text + "')").parent(),
        href = link.attr("href");
        link.remove();
        return href;
    },
    moveItemToMore = function(text) {
    if ($(context).find("ol.gbmcc li.gbmtc a:contains('" + text + "')").size() == 0) {
      var url = removeItem(text);
      addMoreItem(url, text);
    }
    },
    addMoreItem = function(url, text) {
    var item = $("<li />").addClass("gbmtc").append(
                  $("<a />").attr("href", url).addClass("gbmt").attr("target", "_blank").html(text));
        $(context).find("ol.gbmcc li.gbmtc:last-child").prev().before(item);
    },    
    removeMoreItem = function(text) {
    var link = $(context).find("ol.gbmcc li.gbmtc a:contains('" + text + "')"),
        href = link.attr("href");
        link.remove();
        return href;
    },
    moveItemToTop = function(text) {    
    if ($(context).find("a span.gbts:contains('" + text + "')").size() == 0) {
      var url = removeMoreItem(text);
      addItem(url, text);
    }
    };
    
  if (window.top == window.self) {
      var i;
      for (i = 0; i < more.length; i++) {
        moveItemToMore(more[i]);
      }
      for (i = 0; i < top.length; i++) {
        moveItemToTop(top[i]);
      }
  }
})();

I am much happier with this one.  Using the DOM XML functions seems so pre-jQuery.

UPDATE 2012-08-29: GreaseMonkey updated and made Google Reader not display feed items. I added some @grant lines in the header and everything works again. My discussion on Google Groups is located here.

// @grant         GM_getValue
// @grant         GM_setValue

Tuesday, December 27, 2011

SharePoint 2010 Custom Application Page Scroll To Top On Postback

SharePoint 2010 is full of wonderful features that make developers' lives just a bit harder. I ran across an issue where validation was returning a message back to the screen, the page would display the page scrolled to the top and then immediately scroll down to the position to the location of the page prior to posting back. I have had previous run-ins with the s4-workspace, but nothing JavaScript related. I tried several avenues for solutions:

1. Setting the page directive attribute "MaintainScrollPosition" to be false
2. Registering a start up script: $(window).scrollTop(0)
3. Registering a start up script: $("#s4-workspace").scrollTop(0)
4. Attempted to register the the functions via a client script block to add a "pageLoaded" event

The short of it was that none of these worked. I decided to dive in to the HTML source and discovered a "_maintainWorkspaceScrollPosition" hidden field. This looked amazingly like the MaintainScrollPosition functionality, I thought I might be on the right path. The "Workspace" term jumped out at me since the SharePoint custom application page's content is in the s4-workspace; I started to get the feeling that this was a SharePoint feature. After searching all the files for the hidden field, I discovered it was only referenced in the SharePoint JavaScript files. Searching the internet did not yield any solutions on ways to disable the feature. So I generated a function that I would execute to scroll the page to the top.

function scrollToTop() {
    $(window).scrollTop(0);
    $("#s4-workspace").scrollTop(0);
    $("#_maintainWorkspaceScrollPosition").val(0);
}

The function above probably does more than required, but I don't control the Master Page and need to make sure the page can tolerate changes to Master Page style changes.

The server side needs to register a script to execute on the post back. This is a simple line that can be thrown about anywhere.

ScriptManager.RegisterStartupScript(this, this.Page.GetType(), "scrollToTop", "scrollToTop();", true)

Wednesday, August 26, 2009

jQuery Predicate Selectors and ASP.NET

Well, I finally get to blog on something that I get to use in my every day life. There is not much information on the internet regarding to jQuery predicate selectors, so I figured I would blog about how I used them.

The need came up when I was trying to move JavaScript into an external file. The code was put on the page because it referenced ASP.NET controls on the page requiring the use of the old dynamic JavaScript/ASP.NET client ID injection pattern. It seemed like jQuery should be able to find the controls given the last part of the ID. After many google searches and changing of terminologies (I can't remember the search that led me to the solution), I ran across a post from Ben Nadel titled Cool jQuery Predicate Selectors. He describes exactly what I was looking for and more:
Start With: ^=
The ^= operator will filter elements whose attribute starts with the given value.

Ends With: $=
The $= operator will filter elements whose attribute ends with the given value.

Contains: *=
The *= operator will filter elements whose attribute contains the given value.

He goes on to demonstrate how to use these tools, but doesn't go into how it can be used in ASP.NET; that's where I pick up.

Using basic JavaScript to reference an ASP.NET control you have to put a block of code in the aspx page. For example:

<script>
aspnetControl = document.getElementById("≶%= serverControl.ClientID %>");
</script>

This is all well and good until there are a large number of ClientID's in your page. They are generally very long and can bloat the size of your rendered page. We can use the predicate selectors to improve this case. Lets assume the ClientID ends with "serverControl", per the example above, and that it renders an html input tag.

<script>
aspnetControl = $("input[id$='serverControl']");
</script>

Short and sweet. This can be refactored and placed into an external file which can be cached.

As for performance, it is safe to assume that the document.getElementById call would be fastest followed by using the jQuery ID selector (which uses the getElementById command) then the predicate selectors. In my real world use of predicate selectors, it seems to be as fast or faster than the the other methods using IE 8 and Firefox 3.0/3.5.

This can go a long way to decreasing the page size and loading/rendering speed of the page. According to Google's Page Speed Best Practices:
You should see a benefit for any file that can be reduced by 25 bytes or more (less than this will not result in any appreciable performance gain).

Monday, November 3, 2008

Navigating Text Boxes In A Grid View With Arrow Keys

To many business people/non-developers, Excel is the best application ever. It's a database, a calculator, a data entry system, and so much more. I have to hold myself back from laughing when I hear phrases like "Excel database" or "Excel application". In fairness, Excel is a flexible, powerful application and it is easy for lay-people to bend to their needs.

Problems arise when a developer is tasked with taking a process and develop it for the web. The web paradigm does not lend itself to reproducing Excel very easily, sometimes not at all.

In my situation, Excel really wasn't a part of the business' original process, but they are most familiar with it. We came up with several ideas and concepts using standard grid view conventions. They were turned down because they were not "Excel". After several conversations to try to get at what exactly the business users did not like (which they could not immediately communicate), it finally came down to the fact that they wanted Excel in a web page.

Since we are using Microsoft Office SharePoint Services, it seemed clear that Excel Services would be the ideal solution. Not having used it before, I did some further investigation and determined that there was much to be desired.

I was forced to go back to the grid view. After more conversations it came to light what functionality we needed to satisfy the business users needs. For the amount of data and entry speed they were looking for, the standard way of navigating the controls was no acceptable. To increase the speed of navigating around the grid, I decided that we could implement arrow key navigation similar to Excel.

Goals & enhancements:
  • Make the arrow key navigation work similar to Excel
  • If a column is not editable, skip it and proceed to the next column with input
  • If the caret is at the edge of the table, wrap around to the other side
  • Make the solution generic so it can be applied to any grid
  • Use javascript objects
  • Use as little code as possible and optimize for the code to be Minified.
So we have our requirements, now lets create our solution. This is a work in progress. I am sure there is much room for improvement, feel free to suggest optimizations.

In the javascript include file:
var Grid = function(table) {
    var obj = (typeof table == 'string') ? $("#"+ table)[0] : table;
    this.length = obj.rows.length;
    this.parent = obj.parentNode;
    this.tbl = obj;
    this.cellLength = (this.length > 0) ? this.row(1).cells.length : 0;
};
Grid.prototype = {
    row: function(idx) {
        return this.tbl.rows[idx];
    },
    cell: function(rowIdx, idx) {
        return this.tbl.rows[rowIdx].cells[idx];
    },
    XYByEl: function(el) {
        var td = ((el.tagName=="TD") ? 
            el : 
            (el.parentNode.tagName=="TD") ? 
                el.parentNode : 
                null);
        return (td) ? 
            {r: td.parentNode.rowIndex, c: td.cellIndex} : 
            {r: 1, c: 0};
    },
    firstChild: function(rowIdx, idx) {
        return this.firstInput(this.cell(rowIdx, idx));
    },
    firstInput: function(c) {
        if (c!=null)
            if (isIE && (c.children[0]!=null)&&( ( (c.children[0].tagName=="INPUT") && ( (c.children[0].type=="text")||(c.children[0].type=="checkbox") ) ) || (c.children[0].tagName=="SELECT"))) {
                return c.children[0];
            } else {
                var cn = c.childNodes;
                if (cn) {
                    for (var idx=0; idx < cn.length; idx++) {
                        if ( (cn[idx].nodeType == 1)&&( ( (cn[idx].tagName=="INPUT") && ( (cn[idx].type=="text")||(cn[idx].type=="checkbox") ) ) || (cn[idx].tagName=="SELECT")) ){//
                            cn[idx].setAttribute('autocomplete','off');
                            return cn[idx];
                        }
                    }
                }
            }
        return null;
    },
    nextInput: function(r, index, adder) {
        var fi = this.firstInput(r.cells[index]);
        if (fi)
            {return this.focusSelect(fi);}
        else if (index+adder >= r.cells.length)
            return this.nextInput(r, 0, adder);
        else if (index+adder < 0)
            return this.nextInput(r, this.cellLength-1, adder);
        else
            return this.nextInput(r, index+adder, adder);
    },
    focusSelect: function(ctl) {
        if (ctl) {
            ctl.focus();
            if (ctl.type=="text"){ctl.select();}
        }
        return ctl;
    },
    cellUp: function(idx, cIdx) {//wrap to bottom when at top
        idx = (idx-1 === 0) ? this.length-1 : idx - 1;
        var newctl = this.firstChild(idx, cIdx);
        return this.focusSelect(newctl);
    },
    cellDown: function(idx, cIdx) {
        var newctl = null;
        if(idx+1 == this.length) { //wrap to top when at bottom
            newctl = this.firstChild(1, cIdx);
            this.parent.scrollTop = 0;
        } else {
            newctl = this.firstChild(idx + 1, cIdx);
        }
        return this.focusSelect(newctl);
    },
    cellLeft: function(idx, cIdx, child) {
        if(caretAtBegin(child)) {
            var newctl = null;
            if (prevKeyUp == 37) {
                newctl = this.nextInput(this.row(idx),cIdx-1, -1);
                if (!caretAtBegin(newctl)) prevKeyUp = -1;
                return this.focusSelect(newctl);
            } else {
                prevKeyUp = 37;
            }
        }
    },
    cellRight: function(idx, cIdx, child) {
        if(caretAtEnd(child)) {
            if (prevKeyUp == 39) {
                if(cIdx == this.cellLength-1) 
                    this.parent.scrollLeft = 0;
                newctl = this.nextInput(this.row(idx),cIdx+1, 1);
                if (!caretAtEnd(newctl)) prevKeyUp = -1; 
                return this.focusSelect(newctl);
            } else {
                prevKeyUp = 39;
            }
        }
    }
};
function getTarget(ev) {
    var t = ev.target || ev.srcElement;
    return (t && 1 == t.nodeType) ? t : null;
}
var prevKeyUp = -1;
function ArrowKeyNav(gridTable, e) {
    if(!e) e=window.event;
    var key = e.keyCode;
    if (/^(?:3[7-9]|40)$/.test(key)) {
        var table = new Grid(ScrollTable),
            targ = getTarget(e),
            newctl = null,
            cellInfo = table.XYByEl(targ),
            Index = cellInfo.r, childIndex = cellInfo.c;
        if (cellInfo)// != null 
            switch(key) {
                case 40: //down
                    newctl = table.cellDown(Index, childIndex);
                    break;
                case 38: //up
                    newctl = table.cellUp(Index,childIndex);
                    break;
                case 37: //left
                    newctl = table.cellLeft(Index, childIndex, targ);
                    break;
                case 39: //right
                    newctl = table.cellRight(Index, childIndex, targ);
                    break;
                default:
                break;
            }//end switch
        return newctl;
     }//if eventKey
}//end fcn 

caretPos = function (control) {
    var iCaretPos = 0;
    if (control){//!=null
        if (document.selection) { //IE Support
            var oSel = document.selection.createRange();
            oSel.moveStart('character', -1*(control.value.length));
            iCaretPos = oSel.text.length;
        } else if (control.selectionStart || control.selectionStart == '0')
            //Firefox Support
            iCaretPos = control.selectionStart;
    }
    return iCaretPos;
};
caretAtEnd = function(control) {
    return ( (control) && ( (control.type=="checkbox")||(control.tagName=="SELECT") ) ) || (caretPos(control) >= control.value.length);
};

caretAtBegin = function(control) {
    return ( (control) && ( (control.type=="checkbox")||(control.tagName=="SELECT") ) ) || (caretPos(control) == 0);
};
In the grid page, put a script block with the following in it:
$(document).ready(function(){
    $("table.NavWithArrows").keyup(function(e){
        return ArrowKeyNav([table client id], e);
    }); 
});
This and a couple other tweaks, satisfied the business' need. The class NavWithArrows would be attached to the grid view table. This code catches the on key up event when it bubbles up the DOM to the table element.

As it turns out, there is a considerable amount of code to make this functionality happen. Hopefully this will save someone many hours of programming and debugging.

The applications of this go beyond a simple grid application. With a few tweaks, this solution can be applied to a genral input form or extended to allow more than one input control in grid cell(this version assumes there is only one control per cell). I'll leave it to others to extend this functionality.

Friday, October 31, 2008

A Simple, Flexible Architecture For Popup Warnings

I have looked around the web for a good way to display on screen warnings and found most examples wanting. They were rather bloaty, required the use of a different library or you had to pay for them. So I set out and with my favorite javascript library, jQuery, and made my own.

My goals were pretty simple:
  • I wanted to use built-in capabilities when I could (e.g. the tooltip/hover title/alt)
  • Use jQuery to speed up the development and make it easily cross-browser compatible
  • Make it small, compact, and as few lines as possible
  • Assume no needed html will be on the page calling the function
This is still a work in progress but it is completely functional in it's current state. In my CSS file:
.popup{
    border:1px solid #CCC;
 position:absolute;
 width:250px;
 border:1px solid #c93;
 background:#ffc;
 padding:5px;
 right:3px;
 top : -175px;
 font-weight:bold;
}
.popup div p
{
 font-weight: normal;
 padding: 3px;
 margin: 0px;
}
.nohref, .popup div p {
 cursor: pointer;
 cursor: hand;
 text-decoration: underline;
}
In my javascript file:
var getScrollXY = function() {
    var w=window, db=document.body, dde=document.documentElement;
    return ( typeof( w.pageYOffset ) == 'number' )          ? [w.pageXOffset, w.pageYOffset] :
            ( db && ( db.scrollLeft || db.scrollTop ) )     ? [db.scrollLeft, db.scrollTop] :
            ( dde && ( dde.scrollLeft || dde.scrollTop ) )  ? [dde.scrollLeft, dde.scrollTop] : [0, 0];
};
varClearCtlBg = function(ctl) {
    ctl.style.background = "#FFF";
    ctl.setAttribute('title', "");
};
var Validate = function (ctl, rule, msg, idsuffix) {
    if (rule && msg && (msg.length>0)) {
    var ctlid = ctl.id+idsuffix;
    ctl.style.background = "#FFF url(http://intellectualponderings.googlecode.com/svn/trunk/blog/images/invalid_line.gif) repeat-x scroll center bottom";
    cTitle = ctl.getAttribute('title');
    if (cTitle.indexOf(msg) == -1) {
        ctl.setAttribute('title', ((cTitle) ? cTitle + "\r\n[ " : "[ ") + msg);
    }
    var warning = "<p id=\"" + ctlid + "_Warn\" onclick=\"focusSelect('" + ctl.id + "');\">" + msg + "</p>";
    if ($("#WarnMsg").length === 0) {
        $("body").append("<div class=\"popup\" id=\"WarnMsg\"><a class=\"nohref\" style=\"float: right;\" onclick=\"$('#WarnMsg').fadeOut('slow').remove();\">X</a>Warning<div>"+ warning +"</div></div>");//
        $("#WarnMsg").show().animate({ top: String(Number(getScrollXY()[1])+3)+"px" }, 750 );//-175
        //$("#closeMessage").click(function() {$("#WarnMsg").fadeOut("slow").remove();}); id=\"closeMessage\" style=\"display: none;\"
    } else if ($("#"+ctlid+"_Warn").length > 0) {
        $("#"+ctlid+"_Warn").fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100);
    } else {
        $("#WarnMsg div").append(warning);
    }
    setTimeout("ClearWarning('"+ctlid+"_Warn')", 15000);
    return true;
    }// Validate - if (rule)
};
var focusSelect = function(ctlid, e) {
    ctl = $("#"+ctlid)[0];
    ctl.focus();if (ctl.type=="text"){ctl.select();}
};
var ClearWarning = function(id) {
    $("#"+id).remove();
    if ($("#WarnMsg div p").length === 0) {
        $("#WarnMsg").hide("slow").remove();
    }    
};

// Lets add a warning function
var required = function(e) {
    ClearCtlBg(this);
    Validate(this, true, "This file is required.");
    Validate(this, (this.value.length > 3), "This file is required.");
};
In my html, I can do something simple:
<html>
<head>
    <!-- Include CSS file -->
    <!-- Include jQuery file -->
    <!-- Include Javascript file -->
</head>
<body>
    <input type="text" id="txtName" class="name" />
    <script type="text/javascript">
    $(document).ready(function(){
        $(".name").blur(required);
        //$(".someddl").change(ddlvalidator);
    });
    </script>
</body>
</html>
To trigger the warning, click in the text box below and click outside the text box. A popup should slide into view on the upper right hand corner. This can be customized by editing the .animate({ top: String(Number(getScrollXY()[1])+3)+"px" }, 750 );.


For example:

It may seem weird to use jQuery to attach a warning to one control. The logic behind this architecture is for use on input tables or Grid Views where every control in a column will have the same class name and each needs to be evaluated.