Attaboy Media

Musings on assorted geekery by Luke Andrews when he's not writing at attaboy.ca.

    Nov 12
    Permalink

    Position.cumulativeOffset() in Safari

    I discovered a workaround today for a bug that was plaguing me: trying to use a script.aculou.us Sortable list in a fixed position box. Works fine on Firefox and IE, but it just wouldn’t behave in Safari — not even the newest version. After playing with it for a while today I realized why it was broken: the position of the element being dragged that was understood by the drag-and-drop code was never consistent with where the element actually was.

    It turns out that Safari (and by extension WebKit) reports the wrong value for the offsetParent attribute — elements with position: fixed shouldn’t have an offset parent, because such elements are fixed to the viewport, and are thus independent of any parent nodes in the HTML tree.

    The prototype.js framework has a method called Position.cumulativeOffset(), which scriptaculous’s drag-and-drop support depends on. Because of the bug in WebKit, the cumulative offset will be calculated wrong any time you have a fixed-position element inside a relative- or absolute-positioned element. That may not be such a common case, but it’s certainly one we use a lot in Dabble DB.

    Fortunately, fixing this problem proved to be relative simple. Prototype already has a workaround built-in for WebKit, so I just added a few lines of code to tell it to stop calculating the offset when it reaches a fixed-position offsetParent element, and instead add the current scroll position of the document.

    Here’s the code. The lines I added are 4 through 9 one (line 2406 in v1.6.0 of prototype.js or line 3265 in v1.5.1.1).

      Element.Methods.cumulativeOffset = function(element) {
        var valueT = 0, valueL = 0;
        do {
          if (Element.getStyle(element, 'position') == 'fixed') {
    // Safari gets fixed position elements' offset parents wrong
            valueT += document.body.scrollTop;
            valueL += document.body.scrollLeft;
            break;
          }
          valueT += element.offsetTop  || 0;
          valueL += element.offsetLeft || 0;
          if (element.offsetParent == document.body)
            if (Element.getStyle(element, 'position') == 'absolute') break;
    
          element = element.offsetParent;
        } while (element);
    
        return Element._returnOffset(valueL, valueT);
      };
    

    Update: the initial code I posted wasn’t quite correct. I’ve updated the code.