views:

4306

answers:

11

I'm using jQuery UI's draggable and droppable libraries in a simple ASP.NET proof of concept application. This page uses the ASP.NET AJAX UpdatePanel to do partial page updates. The page allows a user to drop an item into a trashcan div, which will invoke a postback that deletes a record from the database, then rebinds the list (and other controls) that the item was drug from. All of these elements (the draggable items and the trashcan div) are inside an ASP.NET UpdatePanel.

Here is the dragging and dropping initialization script:

    function initDragging()
    {
        $(".person").draggable({helper:'clone'});
        $("#trashcan").droppable({
            accept: '.person',
            tolerance: 'pointer',
            hoverClass: 'trashcan-hover',
            activeClass: 'trashcan-active',
            drop: onTrashCanned
        });
    }

    $(document).ready(function(){
        initDragging();

        var prm = Sys.WebForms.PageRequestManager.getInstance();
        prm.add_endRequest(function()
        {
            initDragging();
        });
    });

    function onTrashCanned(e,ui)
    {
        var id = $('input[id$=hidID]', ui.draggable).val();
        if (id != undefined)
        {
            $('#hidTrashcanID').val(id);
            __doPostBack('btnTrashcan','');
        }

    }

When the page posts back, partially updating the UpdatePanel's content, I rebind the draggables and droppables. When I then grab a draggable with my cursor, I get an "htmlfile: Unspecified error." exception. I can resolve this problem in the jQuery library by replacing elem.offsetParent with calls to this function that I wrote:

function IESafeOffsetParent(elem)
{
    try
    {
        return elem.offsetParent;
    }
    catch(e)
    {        
        return document.body;
    }
}

I also have to avoid calls to elem.getBoundingClientRect() as it throws the same error. For those interested, I only had to make these changes in the jQuery.fn.offset function in the Dimensions Plugin.

My questions are:

  • Although this works, are there better ways (cleaner; better performance; without having to modify the jQuery library) to solve this problem?
  • If not, what's the best way to manage keeping my changes in sync when I update the jQuery libraries in the future? For, example can I extend the library somewhere other than just inline in the files that I download from the jQuery website.

Update:

@some It's not publicly accessible, but I will see if SO will let me post the relevant code into this answer. Just create an ASP.NET Web Application (name it DragAndDrop) and create these files. Don't forget to set Complex.aspx as your start page. You'll also need to download the jQuery UI drag and drop plug in as well as jQuery core

Complex.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Complex.aspx.cs" Inherits="DragAndDrop.Complex" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
    <script src="jquery-1.2.6.min.js" type="text/javascript"></script>
    <script src="jquery-ui-personalized-1.5.3.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        function initDragging()
        {
            $(".person").draggable({helper:'clone'});
            $("#trashcan").droppable({
                accept: '.person',
                tolerance: 'pointer',
                hoverClass: 'trashcan-hover',
                activeClass: 'trashcan-active',
                drop: onTrashCanned
            });
        }

        $(document).ready(function(){
            initDragging();

            var prm = Sys.WebForms.PageRequestManager.getInstance();
            prm.add_endRequest(function()
            {
                initDragging();
            });
        });

        function onTrashCanned(e,ui)
        {
            var id = $('input[id$=hidID]', ui.draggable).val();
            if (id != undefined)
            {
                $('#hidTrashcanID').val(id);
                __doPostBack('btnTrashcan','');
            }

        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <div>
        <asp:UpdatePanel ID="updContent" runat="server" UpdateMode="Always">
    <ContentTemplate>
        <asp:LinkButton ID="btnTrashcan" Text="trashcan" runat="server" CommandName="trashcan" 
            onclick="btnTrashcan_Click" style="display:none;"></asp:LinkButton>
        <input type="hidden" id="hidTrashcanID" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Save" onclick="Button1_Click" />
        <table>
            <tr>
                <td style="width: 300px;">
                    <asp:DataList ID="lstAllPeople" runat="server" DataSourceID="odsAllPeople"
                        DataKeyField="ID">
                        <ItemTemplate>
                            <div class="person">
                                <asp:HiddenField ID="hidID" runat="server" Value='<%# Eval("ID") %>' />
                                Name:
                                <asp:Label ID="lblName" runat="server" Text='<%# Eval("Name") %>' />
                                <br />
                                <br />
                            </div>
                        </ItemTemplate>
                    </asp:DataList>
                    <asp:ObjectDataSource ID="odsAllPeople" runat="server" SelectMethod="SelectAllPeople" 
                        TypeName="DragAndDrop.Complex+DataAccess" 
                        onselecting="odsAllPeople_Selecting">
                        <SelectParameters>
                            <asp:Parameter Name="filter" Type="Object" />
                        </SelectParameters>
                    </asp:ObjectDataSource>
                </td>
                <td style="width: 300px;vertical-align:top;">
                    <div id="trashcan">
                        drop here to delete
                    </div>
                    <asp:DataList ID="lstPeopleToDelete" runat="server" 
                        DataSourceID="odsPeopleToDelete">
                        <ItemTemplate>
                            ID:
                            <asp:Label ID="IDLabel" runat="server" Text='<%# Eval("ID") %>' />
                            <br />
                            Name:
                            <asp:Label ID="NameLabel" runat="server" Text='<%# Eval("Name") %>' />
                            <br />
                            <br />
                        </ItemTemplate>
                    </asp:DataList>
                    <asp:ObjectDataSource ID="odsPeopleToDelete" runat="server" 
                        onselecting="odsPeopleToDelete_Selecting" SelectMethod="GetDeleteList" 
                        TypeName="DragAndDrop.Complex+DataAccess">
                        <SelectParameters>
                            <asp:Parameter Name="list" Type="Object" />
                        </SelectParameters>
                    </asp:ObjectDataSource>
                </td>
            </tr>
        </table>    
    </ContentTemplate>
    </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

Complex.aspx.cs

namespace DragAndDrop
{
    public partial class Complex : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected List<int> DeleteList
        {
            get
            {
                if (ViewState["dl"] == null)
                {
                    List<int> dl = new List<int>();
                    ViewState["dl"] = dl;

                    return dl;
                }
                else
                {
                    return (List<int>)ViewState["dl"];
                }
            }
        }

        public class DataAccess
        {
            public IEnumerable<Person> SelectAllPeople(IEnumerable<int> filter)
            {
                return Database.SelectAll().Where(p => !filter.Contains(p.ID));
            }

            public IEnumerable<Person> GetDeleteList(IEnumerable<int> list)
            {
                return Database.SelectAll().Where(p => list.Contains(p.ID));
            }
        }

        protected void odsAllPeople_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
        {
            e.InputParameters["filter"] = this.DeleteList;
        }

        protected void odsPeopleToDelete_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
        {
            e.InputParameters["list"] = this.DeleteList;
        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            foreach (int id in DeleteList)
            {
                Database.DeletePerson(id);
            }

            DeleteList.Clear();
            lstAllPeople.DataBind();
            lstPeopleToDelete.DataBind();
        }

        protected void btnTrashcan_Click(object sender, EventArgs e)
        {
            int id = int.Parse(hidTrashcanID.Value);
            DeleteList.Add(id);
            lstAllPeople.DataBind();
            lstPeopleToDelete.DataBind();
        }
    }
}

Database.cs

namespace DragAndDrop
{
    public static class Database
    {
        private static Dictionary<int, Person> _people = new Dictionary<int,Person>();
        static Database()
        {
            Person[] people = new Person[]
            {
                new Person("Chad")
                , new Person("Carrie")
                , new Person("Richard")
                , new Person("Ron")
            };
            foreach (Person p in people)
            {
                _people.Add(p.ID, p);
            }
        }

        public static IEnumerable<Person> SelectAll()
        {
            return _people.Values;
        }

        public static void DeletePerson(int id)
        {
            if (_people.ContainsKey(id))
            {
                _people.Remove(id);
            }
        }

        public static Person CreatePerson(string name)
        {
            Person p = new Person(name);
            _people.Add(p.ID, p);

            return p;
        }
    }

    public class Person
    {
        private static int _curID = 1;
        public int ID { get; set; }
        public string Name { get; set; }
        public Person()
        {
            ID = _curID++;
        }

        public Person(string name)
            : this()
        {
            Name = name;
        }
    }
}
A: 

Hi,

I have exactly the same problem as U. but i can't figure out replacing "elem.offsetParent" by your function, I don't know where to make this replacement. I tried to use "jQuery 1.2.6" and made the change in this file (in "jQuery.fn.offset", line #3369) but I still have the same error message

thanks in advance

+4  A: 

@arilanto - I include this script after my jquery scripts. Performance wise, it's not the best solution, but it is a quick easy work around.

function IESafeOffsetParent(elem)
{
    try
    {
       return elem.offsetParent;
    }
    catch(e)
    {        
      return document.body;
    }
}

// The Offset Method
// Originally By Brandon Aaron, part of the Dimension Plugin
// http://jquery.com/plugins/project/dimensions
jQuery.fn.offset = function() {
/// <summary>
///  Gets the current offset of the first matched element relative to the viewport.
/// </summary>
/// <returns type="Object">An object with two Integer properties, 'top' and 'left'.</returns>

var left = 0, top = 0, elem = this[0], results;

if ( elem ) with ( jQuery.browser ) {
 var parent    = elem.parentNode,
  offsetChild  = elem,
  offsetParent = IESafeOffsetParent(elem),
  doc    = elem.ownerDocument,
  safari2   = safari && parseInt(version) < 522 && !/adobeair/i.test(userAgent),
  css    = jQuery.curCSS,
  fixed  = css(elem, "position") == "fixed";

 // Use getBoundingClientRect if available
 if (false && elem.getBoundingClientRect) {
  var box = elem.getBoundingClientRect();

  // Add the document scroll offsets
  add(box.left + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
   box.top  + Math.max(doc.documentElement.scrollTop,  doc.body.scrollTop));

  // IE adds the HTML element's border, by default it is medium which is 2px
  // IE 6 and 7 quirks mode the border width is overwritable by the following css html { border: 0; }
  // IE 7 standards mode, the border is always 2px
  // This border/offset is typically represented by the clientLeft and clientTop properties
  // However, in IE6 and 7 quirks mode the clientLeft and clientTop properties are not updated when overwriting it via CSS
  // Therefore this method will be off by 2px in IE while in quirksmode
  add( -doc.documentElement.clientLeft, -doc.documentElement.clientTop );

 // Otherwise loop through the offsetParents and parentNodes
 } else {

  // Initial element offsets
  add( elem.offsetLeft, elem.offsetTop );

  // Get parent offsets
  while ( offsetParent ) {
   // Add offsetParent offsets
   add( offsetParent.offsetLeft, offsetParent.offsetTop );

   // Mozilla and Safari > 2 does not include the border on offset parents
   // However Mozilla adds the border for table or table cells
   if ( mozilla && !/^t(able|d|h)$/i.test(offsetParent.tagName) || safari && !safari2 )
    border( offsetParent );

   // Add the document scroll offsets if position is fixed on any offsetParent
   if ( !fixed && css(offsetParent, "position") == "fixed" )
    fixed = true;

   // Set offsetChild to previous offsetParent unless it is the body element
   offsetChild  = /^body$/i.test(offsetParent.tagName) ? offsetChild : offsetParent;
   // Get next offsetParent
   offsetParent = offsetParent.offsetParent;
  }

  // Get parent scroll offsets
  while ( parent && parent.tagName && !/^body|html$/i.test(parent.tagName) ) {
   // Remove parent scroll UNLESS that parent is inline or a table to work around Opera inline/table scrollLeft/Top bug
   if ( !/^inline|table.*$/i.test(css(parent, "display")) )
    // Subtract parent scroll offsets
    add( -parent.scrollLeft, -parent.scrollTop );

   // Mozilla does not add the border for a parent that has overflow != visible
   if ( mozilla && css(parent, "overflow") != "visible" )
    border( parent );

   // Get next parent
   parent = parent.parentNode;
  }

  // Safari <= 2 doubles body offsets with a fixed position element/offsetParent or absolutely positioned offsetChild
  // Mozilla doubles body offsets with a non-absolutely positioned offsetChild
  if ( (safari2 && (fixed || css(offsetChild, "position") == "absolute")) ||
   (mozilla && css(offsetChild, "position") != "absolute") )
    add( -doc.body.offsetLeft, -doc.body.offsetTop );

  // Add the document scroll offsets if position is fixed
  if ( fixed )
   add(Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
    Math.max(doc.documentElement.scrollTop,  doc.body.scrollTop));
 }

 // Return an object with top and left properties
 results = { top: top, left: left };
}

function border(elem) {
 /// <summary>
 ///  This method is internal.
 /// </summary>
 /// <private />
 add( jQuery.curCSS(elem, "borderLeftWidth", true), jQuery.curCSS(elem, "borderTopWidth", true) );
}

function add(l, t) {
 /// <summary>
 ///  This method is internal.
 /// </summary>
 /// <private />
 left += parseInt(l, 10) || 0;
 top += parseInt(t, 10) || 0;
}

return results;
};
CodeChef
NLV
A: 

Dear CodeChef,

I was a little hesitant performing surgery on my jquery, but it solved the same problem for me (drag & drop in IE), and it hasn't broken anything else so far (and our app has 200k daily users). Thank you very much for this.

agha farhad
A: 

@CodeChef

Genius bit of code that - thanks for posting (would vote but not got the rep to do so yet)

Thanks again

Barry Roberts
+1  A: 

hi,

i tried the following workaround for the getBoundingClientRect() unspecified error whilst drag n drop, and it works fine.

in the jquery.1.4.2.js (i.e base jquery file, where the error is thrown exactly)

replace the elem.getBoundingClientRect() function call in js file

//the line which throws the unspecified error

var box = elem.getBoundingClientRect(),

with this..

var box = null; try { box = elem.getBoundingClientRect(); } catch(e) { box = { top : elem.offsetTop, left : elem.offsetLeft } ; }

This solves the issue and drag n drop will work quitely even after post back through update panel

Regards

Raghu

Raghunathan
A: 

@Raghu

Thanks for this bit of code! I was having the same problem in IE and this fixed it.

var box = null; try { box = elem.getBoundingClientRect(); } catch(e) { box = { top : elem.offsetTop, left : elem.offsetLeft } ; }

Dilbert
A: 

You guys are the best. The quick fix works perfectly

coords
A: 

@Raghunathan

Hi, I replaced the following code -

var box = elem.getBoundingClientRect(),

with your code, but my mouseenter code has stopped working. Can you suggest solution to this ?

Anyways, your solution is great....the error had been vary painful to me and I spent whole weekend to fix.....Thanks buddy...:)

KutePHP
A: 

If you would like to fix the minified/compressed .js file for jQuery version 1.4.2, replace:

var d=b.getBoundingClientRect(),

with

var d = null; try { d = b.getBoundingClientRect(); } catch(e) { d = { top : b.offsetTop, left : b.offsetLeft } ; }

(note that there is no comma after the closing brace now)

Dejan nenov
A: 

My version is:

1) Add function: function getOffsetSum(elem) { var top = 0, left = 0 while (elem) { top = top + parseInt(elem.offsetTop) left = left + parseInt(elem.offsetLeft) try { elem = elem.offsetParent } catch (e) { return { top: top, left: left } } } return { top: top, left: left } };

2) replace var box = this[0].getBoundingClientRect() with var box = getOffsetSum(this[0])

PS: jquery-1.3.2.

Konstantin
A: 

Thank you konstantin. your solution working for me

Asik