fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2011-05-12 08:49 am

[dw-free] Contextual hover menu doesn't work on jquerified pages

[commit: http://hg.dwscoalition.org/dw-free/rev/c29f3ee7ce3d]

http://bugs.dwscoalition.org/show_bug.cgi?id=2507

Make contextual hover work on jQuerified pages.

Patch by [personal profile] fu.

Files modified:
  • cgi-bin/DW/BetaFeatures/journaljquery.pm
  • cgi-bin/LJ/S2.pm
  • cgi-bin/LJ/S2/EntryPage.pm
  • cgi-bin/ljdefaults.pl
  • cgi-bin/ljlib.pl
  • cgi-bin/weblib.pl
  • htdocs/js/contextualhover-jquery.js
  • htdocs/js/jquery.ajaxtip.js
  • htdocs/js/jquery.contextualhover.js
  • htdocs/js/jquery.hoverIntent.js
  • htdocs/js/jquery.hoverIntent.minified.js
  • htdocs/stc/contextualhover.css
--------------------------------------------------------------------------------
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/DW/BetaFeatures/journaljquery.pm
--- a/cgi-bin/DW/BetaFeatures/journaljquery.pm	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/DW/BetaFeatures/journaljquery.pm	Thu May 12 16:45:29 2011 +0800
@@ -26,10 +26,10 @@
         "Thread expander",
         "Same-page poll submission",
         "Media embed placeholder expansion",
+        "Contextual hover",
     );
 
     my @notimplemented = (
-        "Contextual hover",
         "Icon browser",
         "Same-page comment tracking",
     );
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/LJ/S2.pm
--- a/cgi-bin/LJ/S2.pm	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/LJ/S2.pm	Thu May 12 16:45:29 2011 +0800
@@ -191,13 +191,8 @@
         # used if we're using our jquery library
         LJ::need_res( { group => "jquery" }, qw(
                         js/md5.js
-
-                        js/jquery.ajaxtip.js
-                        js/tooltip.js
-                        js/tooltip.dynamic.js
-                        stc/ajaxtip.css
-
                         js/login-jquery.js
+
                         js/jquery.poll.js
                         js/jquery.mediaplaceholder.js
                     ) );
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/LJ/S2/EntryPage.pm
--- a/cgi-bin/LJ/S2/EntryPage.pm	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/LJ/S2/EntryPage.pm	Thu May 12 16:45:29 2011 +0800
@@ -382,7 +382,6 @@
 
     LJ::need_res( "js/commentmanage.js" );
     LJ::need_res( { group => "jquery" }, qw(
-            js/jquery/jquery.ui.widget.js
             js/jquery.ajaxtip.js
             js/jquery.commentmanage.js
             js/tooltip.js
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/ljdefaults.pl
--- a/cgi-bin/ljdefaults.pl	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/ljdefaults.pl	Thu May 12 16:45:29 2011 +0800
@@ -354,6 +354,7 @@
         "js/jquery/jquery.ui.sortable.js"   => "js/jquery/jquery.ui.sortable.min.js",
         "js/jquery/jquery.ui.widget.js"     => "js/jquery/jquery.ui.widget.min.js",
 
+        "js/hoverIntent.js"             => "js/hoverIntent.minified.js",
         "js/tooltip.js"                 => "js/tooltip.min.js",
         "js/tooltip.dynamic.js"         => "js/tooltip.dynamic.min.js",
     ) unless defined %LJ::MINIFY;
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/ljlib.pl
--- a/cgi-bin/ljlib.pl	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/ljlib.pl	Thu May 12 16:45:29 2011 +0800
@@ -1337,14 +1337,29 @@
             if LJ::is_enabled('esn_ajax');
 
         # contextual popup JS
-        LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
-                        js/ippu.js
-                        js/lj_ippu.js
-                        js/hourglass.js
-                        js/contextualhover.js
-                        stc/contextualhover.css
-                        ))
-            if $LJ::CTX_POPUP;
+        if ( $LJ::CTX_POPUP ) {
+            LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
+                            js/ippu.js
+                            js/lj_ippu.js
+                            js/hourglass.js
+                            js/contextualhover.js
+                            stc/contextualhover.css
+                            ));
+
+            LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY, group=> 'jquery' },
+                qw(
+                    js/jquery/jquery.ui.widget.js
+
+                    js/jquery.ajaxtip.js
+                    js/tooltip.js
+                    js/tooltip.dynamic.js
+                    stc/ajaxtip.css
+
+                    js/jquery.hoverIntent.js
+                    js/jquery.contextualhover.js
+                    stc/contextualhover.css
+                ));
+        }
 
         # development JS
         LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY }, qw(
diff -r 634318082e89 -r c29f3ee7ce3d cgi-bin/weblib.pl
--- a/cgi-bin/weblib.pl	Thu May 12 16:42:01 2011 +0800
+++ b/cgi-bin/weblib.pl	Thu May 12 16:45:29 2011 +0800
@@ -2567,9 +2567,16 @@
     my $now = time();
     my %list;   # type -> [];
     my %oldest; # type -> $oldest
+    my %included = ();
     my $add = sub {
         my ($type, $what, $modtime) = @_;
 
+        # the same file may have been included twice
+        # if it was in two different groups and not JS
+        # so add another check here
+        next if $included{$what};
+        $included{$what} = 1;
+
         # in the concat-res case, we don't directly append the URL w/
         # the modtime, but rather do one global max modtime at the
         # end, which is done later in the tags function.
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/js/contextualhover-jquery.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/contextualhover-jquery.js	Thu May 12 16:45:29 2011 +0800
@@ -0,0 +1,62 @@
+(function($) {
+    $.widget("ui.contextualhover", {
+        popupDelay: 500,
+        hideDelay: 250,
+    });
+
+    var ContextualHover = {
+        setup: function() {
+            if (!Site || !Site.ctx_popup) return;
+            if (Site.ctx_popup_userhead)
+                ContextualHover._initUserhead();
+    
+            if (Site.ctx_popup_icons)
+                ContextualHover._initIcons();
+        },
+
+        _initUserhead: function() {
+            $("span.ljuser").each(function() {
+                var $usertag = $(this);
+                if ( $usertag.data("userdata") ) return;
+
+                $("img", $usertag).each(function() {
+                    // if the parent (a tag with link to userinfo) has userid in its URL, then
+                    // this is an openid user icon and we should use the userid
+                    var $parent = $(this).parent("a[href]");
+                    var data = {};
+                    var userid;
+                    if (userid = $parent.attr("href").match(/\?userid=(\d+)/i)) 
+                        data.userid = userid[1];
+                    else
+                        data.username = $usertag.attr("lj:user");
+                    if ( !data.username && !data.userid ) return;
+
+                    $usertag.data("userdata", data).addClass("ContextualPopup");
+                });
+            });
+        },
+
+        _initIcons: function() {
+
+            $("img[src*='/userpic/']").each(function() {
+                if ( $(this).data("icon_url") ) return;
+                if (this.src.match(/userpic\..+\/\d+\/\d+/) ||
+                    this.src.match(/\/userpic\/\d+\/\d+/)) {
+                    $(this).data("icon_url", this.src).addClass("ContextualPopup");
+                }
+            });
+        }
+    }
+
+    // for init
+    $.extend({ contextualhover: ContextualHover.setup });
+
+})(jQuery);
+
+// initialize on page load
+$(function() {
+    $.contextualhover();
+    $(".ContextualPopup").live("mousemove", function(e){
+        console.log(e.target);
+    });
+});
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/js/jquery.ajaxtip.js
--- a/htdocs/js/jquery.ajaxtip.js	Thu May 12 16:42:01 2011 +0800
+++ b/htdocs/js/jquery.ajaxtip.js	Thu May 12 16:45:29 2011 +0800
@@ -1,7 +1,8 @@
 (function($) {
 $.widget("dw.ajaxtip", {
     options: {
-        content: undefined
+        content: undefined,
+        tooltip: { dynamic: true }
     },
     _namespace: function() {
         return this.options.namespace ? "."+this.options.namespace : "";
@@ -10,31 +11,31 @@
         var self = this;
         var ns = self._namespace();
 
-        var tipcontainer = $("<div class='ajaxtooltip' style='display: none'></div>")
+        var tipcontainer = $("<div class='ajaxtooltip ajaxtip' style='display: none'></div>")
                         .click(function(e) {e.stopPropagation()})
 
         self.element
             .after(tipcontainer)
             .bind("ajaxresult"+ns, function(e) {
-                self.element.data("tooltip").getTip()
-                     .addClass("ajaxresult-" + e.ajaxresult.status)
-                     .text(e.ajaxresult.message)
+                var tip = self.element.data("tooltip").getTip()
+                     .addClass("ajaxresult-" + e.ajaxresult.status);
+                if ( e.ajaxresult.message ) tip.text(e.ajaxresult.message);
             })
-            .tooltip({
+            .tooltip($.extend({
                 predelay: 0,
                 delay: 1500,
                 events: {
-                    def: "ajaxstart"+ns+",ajaxresult"+ns,
+                    def: "ajaxstart"+ns+", tooltipout"+ns+" ajaxresult"+ns,
                     widget: "ajaxstart"+ns+",ajaxresult"+ns,
                     tooltip: "mouseover,mouseleave"
                 },
                 position: "bottom center",
                 relative: true,
                 effect: "fade",
-
                 onBeforeShow: function(e) {
                     var tip = this.getTip();
-                    tip.removeClass("ajaxresult ajaxresult-success ajaxresult-error");
+                    tip.removeClass("ajaxresult ajaxresult-success ajaxresult-error")
+                        .appendTo("body");
 
                     if ( self.options.content && ! this.inprogress ){
                         tip.html(self.options.content)
@@ -43,8 +44,10 @@
                             .addClass("ajaxresult")
                     }
                 }
-            })
-            .dynamic({ classNames: "tip-top tip-right tip-bottom tip-left" });
+            },  self.options.tooltip));
+            if ( self.options.tooltip.dynamic )
+                self.element.dynamic({ classNames: "tip-top tip-right tip-bottom tip-left" });
+
     },
     _init: function() {
         if(this.options.content)
@@ -64,6 +67,11 @@
         var tip = this.element.data("tooltip");
         if( tip && tip.isShown() ) tip.hide();
      },
+    show: function() {
+        var tip = this.element.data("tooltip");
+        tip.show();
+        this.success(/*no msg*/);
+    },
     load: function(opts) {
         var self = this;
 
@@ -77,7 +85,7 @@
         self.element.trigger("ajaxstart" + self._namespace());
 
         $.ajax({
-            type: "POST",
+            type: opts.formmethod || "POST",
             url : opts.endpoint ? self._endpointurl( opts.endpoint) : opts.url,
             data: opts.data,
 
@@ -107,7 +115,7 @@
 
 $.extend( $.dw.ajaxtip, {
     closeall: function() {
-        $(".ajaxtooltip:visible").each(
+        $(".ajaxtip:visible").each(
             function(){
                 var tip = $(this).prev().data("tooltip");
                 if ( !tip.inprogress ) tip.hide()
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/js/jquery.contextualhover.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/jquery.contextualhover.js	Thu May 12 16:45:29 2011 +0800
@@ -0,0 +1,376 @@
+(function($, Site){
+
+$.fn.contextualhoversetup = function() {
+    if (!Site || !Site.ctx_popup) return;
+
+    this.each(function() {
+        if (Site.ctx_popup_userhead)
+            _initUserhead(this);
+
+        if (Site.ctx_popup_icons)
+            _initIcons(this);
+    });
+};
+
+function _initUserhead(context) {
+    $("span.ljuser",context).each(function() {
+        var $usertag = $(this);
+
+        $("img", $usertag).each(function() {
+            // if the parent (a tag with link to userinfo) has userid in its URL, then
+            // this is an openid user icon and we should use the userid
+            var $head = $(this);
+            var $parent = $head.parent("a[href]");
+            var data = {};
+            var userid;
+            if (userid = $parent.attr("href").match(/\?userid=(\d+)/i))
+                data.userid = userid[1];
+            else
+                data.username = $usertag.attr("lj:user");
+            if ( !data.username && !data.userid ) return;
+
+            data.type = "user";
+            $head.contextualhover( data );
+        });
+    });
+}
+
+function _initIcons(context) {
+    $("img[src*='/userpic/']",context).each(function() {
+        var $icon = $(this);
+        if (this.src.match(/userpic\..+\/\d+\/\d+/) ||
+            this.src.match(/\/userpic\/\d+\/\d+/)) {
+            $icon.contextualhover({ "icon_url": this.src, type: "icon" });
+        }
+    });
+}
+
+})(jQuery, Site);
+
+$.widget("dw.contextualhover", {
+options: {
+    disableAJAX: false,
+
+    username: undefined,
+    userid: undefined,
+    icon_url: undefined,
+    type: undefined // icon or user(head)
+},
+_create: function() {
+    var self = this;
+    var opts = self.options;
+
+    if ( opts.type == "icon" ) {
+        self.element.removeAttr("title");
+        var parent = self.element.parent("a")
+        if ( parent.length > 0 )
+            self.element = parent;
+    }
+
+    var trigger = self.element;
+    trigger.addClass("ContextualPopup");
+
+    var action = $.fn.hoverIntent ? "hoverIntent" : "hover";
+    trigger[action](
+    function() {
+        if ( self.cachedResults ) {
+            self._renderPopup();
+            self._trigger("complete");
+            return;
+        }
+
+        trigger.ajaxtip({ namespace: "contextualpopup", tooltip: {position: "bottom left",dynamic:false} })
+            .ajaxtip( "load", {
+                endpoint: "ctxpopup",
+                formmethod: "GET",
+                data: {
+                    "user": opts.username || "",
+                    "userid": opts.userid || 0,
+                    "userpic_url": opts.icon_url || "",
+                    "mode": "getinfo"
+                },
+                success: function( data, status, jqxhr ) {
+                    if ( data.error ) {
+                        if ( data.noshow )
+                            trigger.ajaxtip( "cancel" );
+                        else
+                            trigger.ajaxtip( "error", data.error )
+                    } else {
+                        self.cachedResults = data;
+                        self._renderPopup();
+
+                        // expire cache after 5 minutes
+                        setTimeout(function() {
+                            self.cachedResults = null;
+                        }, 60 * 5 * 1000);
+                    }
+                    self._trigger("complete");
+                },
+                error: function( jqxhr, status, error ) {
+                    trigger.ajaxtip( "error", "Error contacting server. " + error);
+                    self._trigger( "complete" );
+                }
+            });
+    },
+    function() {
+    }
+    )
+},
+
+_renderPopup: function() {
+    var self = this;
+    var opts = self.options;
+    var data = self.cachedResults;
+
+    if ( data && ( !data.username || !data.success || data.noshow ) ) {
+        this.element.ajaxtip("cancel");
+    }
+
+    this.element.ajaxtip("show")
+
+    var $inner = $("<div class='Inner'></div>");
+    var $content = $("<div class='Content'></div>");
+
+    if ( data.url_userpic ) {
+        var $link = $("<a></a>", { href: data.url_allpics });
+        var $icon = $("<img>", { src: data.url_userpic }).attr({width: data.userpic_w, height: data.userpic_h});
+        var $container = $("<div class='Userpic'></div>").append($link.append($icon));
+
+        $inner.append($container);
+    }
+
+    $inner.append($content);
+
+    var username = data.display_username;
+    var $relation = $("<div class='Relation'></div>");
+    var strings = {
+        member: "You are a member of " + username,
+        watching: "You have subscribed to " + username,
+        watched_by: username + " has subscribed to you",
+        mutual_watch: username + " and you have mutual subscriptions",
+        trusting: "You have granted access to " + username,
+        trusted_by: username + " has granted access to you",
+        mutual_trust: username + " and you have mutual access",
+        self: "This is you"
+    };
+    if ( data.is_comm ) {
+        var rels = [];
+        if (data.is_member) rels.push(strings.member);
+        if (data.is_watching) rels.push(strings.watching);
+
+        $relation.html(rels.length > 0 ? rels.join("<br />") : username);
+    } else if (data.is_syndicated ) {
+        $relation.html(data.is_watching ? strings.watching : username);
+    } else if (data.is_requester) {
+        $relation.html( strings.self );
+    } else {
+        var rels = [];
+        if ( data.is_trusting && data.is_trusted_by )
+            rels.push(strings.mutual_trust);
+        else if ( data.is_trusting )
+            rels.push(strings.trusting);
+        else if ( data.is_trusted_by )
+            rels.push(strings.trusted_by);
+
+        if ( data.is_watching && data.is_watched_by )
+            rels.push(strings.mutual_watch);
+        else if ( data.is_watching )
+            rels.push(strings.watching);
+        else if ( data.is_watched_by )
+            rels.push(strings.watched_by);
+
+        $relation.html(rels.length > 0 ? rels.join("<br />") : username);
+    }
+    $content.append($relation);
+
+    if ( data.is_logged_in && data.is_comm ) {
+        var $membership = $("<span></span>");
+        if ( ! data.is_closed_membership || data.is_member ) {
+            var $membershiplink = $("<a></a>");
+            var $membershipaction = data.is_member ? "leave" : "join";
+
+            if ( data.is_member )
+                $membershiplink.attr("href" , data.url_leavecomm ).html("Leave");
+            else
+                $membershiplink.attr("href", data.url_joincomm ).html("Join community");
+
+            if ( ! opts.disableAJAX ) {
+                $membershiplink.click(function(e) {
+                    e.stopPropagation(); e.preventDefault();
+                    self._changeRelation(data, membership_action, this, e);
+                });
+            }
+
+            $membership.append($membershiplink);
+        } else {
+            $membership.html("Community closed");
+        }
+        $content.append($membership);
+    }
+
+    var links = [];
+    if ( data.is_logged_in && ( data.is_person || data.is_identity ) && data.can_message ) {
+        var $sendmessage = $("<a></a>", { href: data.url_message }).html("Send message");
+        links.push($("<span></span>").append($sendmessage));
+    }
+
+    // relationships
+    if ( data.is_logged_in && ! data.is_requester ) {
+        if ( ! data.is_trusting ) {
+            if ( data.is_person || data.other_is_identity ) {
+                var $addtrust = $("<a></a>", { href: data.url_addtrust } ).html("Grant access");
+                links.push($("<span class='AddTrust'></span>").append($addtrust));
+
+                if( ! opts.disableAJAX ) {
+                    $addtrust.click(function(e) {
+                        e.stopPropagation(); e.preventDefault();
+                        self._changeRelation(data, "addTrust", this, e);
+                    });
+                }
+            }
+        } else {
+            if ( data.is_person || data.other_is_identity ) {
+                var $removetrust = $("<a></a>", { href: data.url_addtrust } ).html("Remove access");
+                links.push($("<span class='RemoveTrust'></span>").append($removetrust));
+
+                if( ! opts.disableAJAX ) {
+                    $removetrust.click(function(e) {
+                        e.stopPropagation(); e.preventDefault();
+                        self._changeRelation(data, "removeTrust", this, e);
+                    });
+                }
+            }
+        }
+
+        if ( !data.is_watching && !data.other_is_identity ) {
+            var $addwatch = $("<a></a>", { href: data.url_addwatch } ).html("Subscribe");
+            links.push($("<span class='AddWatch'></span>").append($addwatch));
+
+            if( ! opts.disableAJAX ) {
+                $addwatch.click(function(e) {
+                    e.stopPropagation(); e.preventDefault();
+                    self._changeRelation(data, "addWatch", this, e);
+                });
+            }
+        } else if ( data.is_watching ) {
+            var $removewatch = $("<a></a>", { href: data.url_addwatch } ).html("Remove subscription");
+            links.push($("<span class='RemoveWatch'></span>").append($removewatch));
+
+            if( ! opts.disableAJAX ) {
+                $removewatch.click(function(e) {
+                    e.stopPropagation(); e.preventDefault();
+                    self._changeRelation(data, "removeWatch", this, e);
+                });
+            }
+        }
+        $relation.addClass("RelationshipStatus");
+    }
+
+    // FIXME: double-check this when vgifts come out
+    if ( ( data.is_person || data.is_comm ) && ! data.is_requester && data.can_receive_vgifts ) {
+        var $sendvgift = $("<a></a>", { href: Site.siteroot + "/shop/vgift?to=" + data.username })
+            .html("Send a virtual gift");
+        links.push($("<span></span").append($sendvgift));
+    }
+
+    if ( data.is_logged_in && ! data.is_requester && ! data.is_syndicated ) {
+        if ( data.is_banned ) {
+            var $unbanlink = $("<a></a>", { href: Site.siteroot + "/manage/banusers" });
+            $unbanlink.html( data.is_comm ? "Unban community" : "Unban user" );
+            if( ! opts.disableAJAX ) {
+                $unbanlink.click(function(e) {
+                    e.stopPropagation(); e.preventDefault();
+                    self._changeRelation(data, "setUnban", this, e);
+                });
+            }
+            links.push($("<span class='SetUnban'></span>").append($unbanlink));
+
+        } else {
+            var $banlink = $("<a></a>", { href: Site.siteroot + "/manage/banusers" });
+            $banlink.html( data.is_comm ? "Ban community" : "Ban user" );
+            if( ! opts.disableAJAX ) {
+                $banlink.click(function(e) {
+                    e.stopPropagation(); e.preventDefault();
+                    self._changeRelation(data, "setBan", this, e);
+                });
+            }
+            links.push($("<span class='SetBan'></span>").append($banlink));
+        }
+    }
+
+    var linkslength = links.length;
+    $.each(links,function(index) {
+        $content.append(this);
+        $content.append("<br>");
+    });
+
+    $("<span>View: </span>").appendTo($content);
+    if ( data.is_person || data.is_comm || data.is_syndicated ) {
+        var $journallink = $("<a></a>", {href: data.url_journal});
+        if (data.is_person)
+            $journallink.html("Journal");
+        else if ( data.is_comm )
+            $journallink.html("Community");
+        else if ( data.is_syndicated )
+            $journallink.html("Feed");
+
+        $content.append(
+                $journallink,
+                $("<span> | </span>"),
+                $("<a></a>", { href: data.url_profile} ).html("Profile")
+        );
+    }
+
+    $content.append($("<div class='ljclear'>&nbsp;</div>"));
+
+    this.element
+        .ajaxtip("widget")
+            .removeClass("ajaxresult ajaxtooltip").addClass("ContextualPopup")
+            .empty().append($inner)
+},
+
+_changeRelation: function(info, action, link, e) {
+    if ( !info ) return;
+    var self = this;
+    var $link = $(link);
+
+    $link.ajaxtip({namespace: "changerelation"}).ajaxtip("load", {
+        endpoint: "changerelation",
+        data: {
+            target: info.username,
+            action: action,
+            auth_token: info[action+"_authtoken"]
+        },
+        success: function( data, status, jqxhr ) {
+            if ( data.error ) {
+                $link.ajaxtip( "error", data.error )
+            } else if ( ! data.success ) {
+                $link.ajaxtip( "error", "Did not change relation successfully" );
+                self._renderPopup(data);
+            } else {
+                if ( self.cachedResults ) {
+                    var updatedProps = [ "is_trusting", "is_watching", "is_member", "is_banned" ];
+                    $.each(updatedProps,function(){
+                        self.cachedResults[this]=data[this];
+                    });
+                }
+                self._renderPopup();
+            }
+            self._trigger("complete");
+        },
+        error: function( jqxhr, status, error ) {
+            $link.ajaxtip( "error", "Error contacting server. " + error);
+            self._trigger( "complete" );
+        }
+    });
+}
+
+});
+
+jQuery(document).ready(function($){
+    $(document).contextualhoversetup();
+    $(document.body).delegate( "*", "updatedcontent.entry.poll.comment", function(e) {
+        e.stopPropagation();
+        $(this).contextualhoversetup();
+    });
+});
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/js/jquery.hoverIntent.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/jquery.hoverIntent.js	Thu May 12 16:45:29 2011 +0800
@@ -0,0 +1,106 @@
+/**
+* hoverIntent is similar to jQuery's built-in "hover" function except that
+* instead of firing the onMouseOver event immediately, hoverIntent checks
+* to see if the user's mouse has slowed down (beneath the sensitivity
+* threshold) before firing the onMouseOver event.
+* 
+* hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+
+* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
+* 
+* hoverIntent is currently available for use in all personal or commercial 
+* projects under both MIT and GPL licenses. This means that you can choose 
+* the license that best suits your project, and use it accordingly.
+* 
+* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
+* $("ul li").hoverIntent( showNav , hideNav );
+* 
+* // advanced usage receives configuration object only
+* $("ul li").hoverIntent({
+*	sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
+*	interval: 100,   // number = milliseconds of polling interval
+*	over: showNav,  // function = onMouseOver callback (required)
+*	timeout: 0,   // number = milliseconds delay before onMouseOut function call
+*	out: hideNav    // function = onMouseOut callback (required)
+* });
+* 
+* @param  f  onMouseOver function || An object with configuration options
+* @param  g  onMouseOut function  || Nothing (use configuration options object)
+* @author    Brian Cherne brian(at)cherne(dot)net
+*/
+(function($) {
+	$.fn.hoverIntent = function(f,g) {
+		// default configuration options
+		var cfg = {
+			sensitivity: 7,
+			interval: 100,
+			timeout: 0
+		};
+		// override configuration options with user supplied object
+		cfg = $.extend(cfg, g ? { over: f, out: g } : f );
+
+		// instantiate variables
+		// cX, cY = current X and Y position of mouse, updated by mousemove event
+		// pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
+		var cX, cY, pX, pY;
+
+		// A private function for getting mouse position
+		var track = function(ev) {
+			cX = ev.pageX;
+			cY = ev.pageY;
+		};
+
+		// A private function for comparing current and previous mouse position
+		var compare = function(ev,ob) {
+			ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+			// compare mouse positions to see if they've crossed the threshold
+			if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
+				$(ob).unbind("mousemove",track);
+				// set hoverIntent state to true (so mouseOut can be called)
+				ob.hoverIntent_s = 1;
+				return cfg.over.apply(ob,[ev]);
+			} else {
+				// set previous coordinates for next time
+				pX = cX; pY = cY;
+				// use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
+				ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
+			}
+		};
+
+		// A private function for delaying the mouseOut function
+		var delay = function(ev,ob) {
+			ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
+			ob.hoverIntent_s = 0;
+			return cfg.out.apply(ob,[ev]);
+		};
+
+		// A private function for handling mouse 'hovering'
+		var handleHover = function(e) {
+			// copy objects to be passed into t (required for event object to be passed in IE)
+			var ev = jQuery.extend({},e);
+			var ob = this;
+
+			// cancel hoverIntent timer if it exists
+			if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
+
+			// if e.type == "mouseenter"
+			if (e.type == "mouseenter") {
+				// set "previous" X and Y position based on initial entry point
+				pX = ev.pageX; pY = ev.pageY;
+				// update "current" X and Y position based on mousemove
+				$(ob).bind("mousemove",track);
+				// start polling interval (self-calling timeout) to compare mouse coordinates over time
+				if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
+
+			// else e.type == "mouseleave"
+			} else {
+				// unbind expensive mousemove event
+				$(ob).unbind("mousemove",track);
+				// if hoverIntent state is true, then call the mouseOut function after the specified delay
+				if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
+			}
+		};
+
+		// bind the function to the two event listeners
+		return this.bind('mouseenter',handleHover).bind('mouseleave',handleHover);
+	};
+})(jQuery);
\ No newline at end of file
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/js/jquery.hoverIntent.minified.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/jquery.hoverIntent.minified.js	Thu May 12 16:45:29 2011 +0800
@@ -0,0 +1,9 @@
+/**
+* hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+
+* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
+* 
+* @param  f  onMouseOver function || An object with configuration options
+* @param  g  onMouseOut function  || Nothing (use configuration options object)
+* @author    Brian Cherne brian(at)cherne(dot)net
+*/
+(function($){$.fn.hoverIntent=function(f,g){var cfg={sensitivity:7,interval:100,timeout:0};cfg=$.extend(cfg,g?{over:f,out:g}:f);var cX,cY,pX,pY;var track=function(ev){cX=ev.pageX;cY=ev.pageY};var compare=function(ev,ob){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t);if((Math.abs(pX-cX)+Math.abs(pY-cY))<cfg.sensitivity){$(ob).unbind("mousemove",track);ob.hoverIntent_s=1;return cfg.over.apply(ob,[ev])}else{pX=cX;pY=cY;ob.hoverIntent_t=setTimeout(function(){compare(ev,ob)},cfg.interval)}};var delay=function(ev,ob){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t);ob.hoverIntent_s=0;return cfg.out.apply(ob,[ev])};var handleHover=function(e){var ev=jQuery.extend({},e);var ob=this;if(ob.hoverIntent_t){ob.hoverIntent_t=clearTimeout(ob.hoverIntent_t)}if(e.type=="mouseenter"){pX=ev.pageX;pY=ev.pageY;$(ob).bind("mousemove",track);if(ob.hoverIntent_s!=1){ob.hoverIntent_t=setTimeout(function(){compare(ev,ob)},cfg.interval)}}else{$(ob).unbind("mousemove",track);if(ob.hoverIntent_s==1){ob.hoverIntent_t=setTimeout(function(){delay(ev,ob)},cfg.timeout)}}};return this.bind('mouseenter',handleHover).bind('mouseleave',handleHover)}})(jQuery);
\ No newline at end of file
diff -r 634318082e89 -r c29f3ee7ce3d htdocs/stc/contextualhover.css
--- a/htdocs/stc/contextualhover.css	Thu May 12 16:42:01 2011 +0800
+++ b/htdocs/stc/contextualhover.css	Thu May 12 16:45:29 2011 +0800
@@ -3,6 +3,7 @@
     margin: 5px 0 0 20px;
     font: normal 11px "Arial", "Verdana", sans-serif !important;
     text-align: left;
+    z-index: 99;
     }
 
 * html div.ContextualPopup {
--------------------------------------------------------------------------------