Wednesday, May 28, 2008

Absolute coordinates of DOM element within document

The problem

Sometimes (especially in AJAX projects) it is necessary to get the position of some DOM element in "absolute" coordinates within current document. 
For example such "absolute" position is needed if you would like to show some hidden DIV object exactly on the position (or with some offset) of another element. We use this function in our EasyQuery.NET WebForms library to show popup menu under some condition element (you can see an example here).

The solution

Such properties as style.left, style.top or offsetLeft, offsetTop can be used to get (or set) the position of element within its parent. So to get absolute element's position within document we should move upward on element's tree and add the position of all element's parents (except the latest document element).

However it is not quite easy. There are still some problems:
  1. First, we need to take into account possible scrolling in element's parents and decrease our result accordingly.
  2. Second, there are some distinctions in behavior of different browsers (as usual :-( ). For Internet Explorer we always can just subtract scrolling position of the object stored in element's offsetParent prooperty. But for FireFox we also need to take into consideration all parents accessible by parentNode properties.
  3. Finally, we should take into account the border width for some parent elements. Unfortunately this task is not so easy as it can be supposed especially for Internet Explorer browser.
So here is the function we get in result:

 
function __getIEVersion() {
    var rv = -1; // Return value assumes failure.
    if (navigator.appName == 'Microsoft Internet Explorer') {
        var ua = navigator.userAgent;
        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
        if (re.exec(ua) != null)
            rv = parseFloat(RegExp.$1);
    }
    return rv;
}

function __getOperaVersion() {
    var rv = 0; // Default value
    if (window.opera) {
        var sver = window.opera.version();
        rv = parseFloat(sver);
    }
    return rv;
}

var __userAgent = navigator.userAgent;
var __isIE =  navigator.appVersion.match(/MSIE/) != null;
var __IEVersion = __getIEVersion();
var __isIENew = __isIE && __IEVersion >= 8;
var __isIEOld = __isIE && !__isIENew;

var __isFireFox = __userAgent.match(/firefox/i) != null;
var __isFireFoxOld = __isFireFox && ((__userAgent.match(/firefox\/2./i) != null) || (__userAgent.match(/firefox\/1./i) != null));
var __isFireFoxNew = __isFireFox && !__isFireFoxOld;

var __isWebKit =  navigator.appVersion.match(/WebKit/) != null;
var __isChrome =  navigator.appVersion.match(/Chrome/) != null;
var __isOpera =  window.opera != null;
var __operaVersion = __getOperaVersion();
var __isOperaOld = __isOpera && (__operaVersion < 10);

function __parseBorderWidth(width) {
    var res = 0;
    if (typeof(width) == "string" && width != null && width != "" ) {
        var p = width.indexOf("px");
        if (p >= 0) {
            res = parseInt(width.substring(0, p));
        }
        else {
       //do not know how to calculate other values (such as 0.5em or 0.1cm) correctly now
      //so just set the width to 1 pixel
            res = 1; 
        }
    }
    return res;
}


//returns border width for some element
function __getBorderWidth(element) {
 var res = new Object();
 res.left = 0; res.top = 0; res.right = 0; res.bottom = 0;
 if (window.getComputedStyle) {
  //for Firefox
  var elStyle = window.getComputedStyle(element, null);
  res.left = parseInt(elStyle.borderLeftWidth.slice(0, -2));  
  res.top = parseInt(elStyle.borderTopWidth.slice(0, -2));  
  res.right = parseInt(elStyle.borderRightWidth.slice(0, -2));  
  res.bottom = parseInt(elStyle.borderBottomWidth.slice(0, -2));  
 }
 else {
  //for other browsers
  res.left = __parseBorderWidth(element.style.borderLeftWidth);
  res.top = __parseBorderWidth(element.style.borderTopWidth);
  res.right = __parseBorderWidth(element.style.borderRightWidth);
  res.bottom = __parseBorderWidth(element.style.borderBottomWidth);
 }
   
 return res;
}


//returns the absolute position of some element within document
function getElementAbsolutePos(element) {
 var res = {};
 res.x = 0; res.y = 0;
 if (element !== null) { 
  //use getBoundingClientRect function available in new browsers
  if (element.getBoundingClientRect) {
          var box = element.getBoundingClientRect();
      
       var body = document.body;
       var docElem = document.documentElement;
       
       var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
       var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
       
       var clientTop = docElem.clientTop || body.clientTop || 0;
       var clientLeft = docElem.clientLeft || body.clientLeft || 0;
        

      res.x = Math.round(box.left + scrollLeft - clientLeft);
      res.y = Math.round(box.top + scrollTop - clientTop);
      
  }
  else { //for old browsers
   res.x = element.offsetLeft;
   res.y = element.offsetTop;
   
   var parentNode = element.parentNode;
   var borderWidth = null;

   while (offsetParent != null) {
    res.x += offsetParent.offsetLeft;
    res.y += offsetParent.offsetTop;
    
    var parentTagName = offsetParent.tagName.toLowerCase(); 

    if ((__isIEOld && parentTagName != "table") || ((__isFireFoxNew || __isChrome) && parentTagName == "td")) {      
     borderWidth = __getBorderWidth(offsetParent);
     res.x += borderWidth.left;
     res.y += borderWidth.top;
    }
    
    if (offsetParent != document.body && offsetParent != document.documentElement) {
     res.x -= offsetParent.scrollLeft;
     res.y -= offsetParent.scrollTop;
    }


    //next lines are necessary to fix the problem with offsetParent
    if (!__isIE && !__isOperaOld || __isIENew) {
     while (offsetParent != parentNode && parentNode !== null) {
      res.x -= parentNode.scrollLeft;
      res.y -= parentNode.scrollTop;
      if (__isFireFoxOld || __isWebKit) {
       borderWidth = __getBorderWidth(parentNode);
       res.x += borderWidth.left;
       res.y += borderWidth.top;
      }
      parentNode = parentNode.parentNode;
     }    
    }

    parentNode = offsetParent.parentNode;
    offsetParent = offsetParent.offsetParent;
   }
  }
 }
    return res;
}

To use this function just pass your element in function's parameter and get the result object with left and top coordinates stored in x and y properties accordingly:
   
     var pos = getElementAbsolutePos(myElement);  
     window.alert("Element's left: " + pos.x " and top: " + pos.y);  

GetElementAbsolutePos function was tested on all most used browsers:

  • Internet Explorer 7.0 and higher
  • FireFox 2.x and FireFox 3.x.
  • Opera 9.x, 10.x
  • Chrome 5.0

Saturday, May 10, 2008

Update ListBox item in Windows Forms

Problem description

Sometimes in Windows Forms applications you use the following code pattern to fill some ListBox control with necessary data:
 
public class MyObject {
  
 private string text;

 public string Text {
  get { return text; }
  set { text = value; }
 }

 public MyObject(string text) {
  this.text = text;
 }
  

 public override string ToString() {
  return this.text;
 }

}

.  .  .  .  .  .  .

 myListBox.Items.Add(new MyObject("Item 1"));
 myListBox.Items.Add(new MyObject("Item 2"));
 myListBox.Items.Add(new MyObject("Item 3"));
.  .  .  .  .  .  .


Now let's suppose you need to change some item and set new value to it's Text property:
 
.  .  .  .  .  .  .
 MyObject item = (MyObject)myListBox.Items[0];
 item.Text = "New value";
 //Corresponding item in list box is not updated accordingly.
.  .  .  .  .  .  .

  
However the corresponding item in your ListBox will not reflect to such change even if you manually "tell" the ListBox to update (through Update or Refresh methods). The solution The right solution fot this problem is to use data binding (you can read this MSDN article for more information). But very often it is too complicated way for such simple situation so I would like to propose you some gimmick which allows to solve the problem without all that binding stuff (and it coud work even faster in most cases). We just need to re-assign modified object to necessary item in the list. So I have wrotten the following simple function:
 
 private void UpdateListBoxItem(ListBox lb, object item) {
  int index = lb.Items.IndexOf(item);
  int currIndex = lb.SelectedIndex;
  lb.BeginUpdate();
  try {
   lb.ClearSelected();
   lb.Items[index] = item;
   lb.SelectedIndex = currIndex;
  }
  finally {
   lb.EndUpdate();
  }
 }


 
Now when I need to update modified item in the list I just call this function with my ListBox object at first parameter and an update item object in second:
.  .  .  .  .  .  .
 MyObject item = (MyObject)myListBox.Items[0];
 item.Text = "New value";
 UpdateListBoxItem(myListBox, item);
.  .  .  .  .  .  .
     
As I said before, this is not quite correct solution but it does what we need and it helped me in many cases.