[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
fu.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=2507
Make contextual hover work on jQuerified pages.
Patch by
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
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'> </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 { --------------------------------------------------------------------------------