Get reference to hovered element in jQuery UI Tooltip

Fabrício Matté picture Fabrício Matté · Apr 22, 2013 · Viewed 8.1k times · Source

Basically, I'm attaching an open handler to my jQuery UI tooltip which performs some checks on the element that triggered the tooltip. What I've got so far:

$(document).tooltip({
  open: function(e, ui) {
    var el = e.toElement/* || e.relatedTarget*/;
    console.log(el.offsetWidth, el.scrollWidth);
    if (el.offsetWidth === el.scrollWidth) {
      ui.tooltip.hide();
    }
  }
});

The check above prevents the tooltip from appearing unless the element is overflowing horizontally, part of a fluid layout. It works nicely in Chrome as you can see in this jsBin.

However, in Firefox, event.toElement is undefined. From reading threads around SO, I've thought that event.relatedTarget would be a suitable substitute, but it is not. While event.toElement references the currently hovered item, the event.relatedTarget in a mouseover references the element which the pointing device exited, which is the correct behavior as per W3C spec (similar to Chrome's event.fromTarget).

I've also tried event.target, event.currentTarget and the this reference but these point to document as it is the node which has the tooltip event handlers bound to. Going through the Tooltip API page didn't help either.

I'm not sure whether I'm overlooking something extremely basic, or if I should try even less orthodox methods.

Is there a way to get a reference to the element that triggered the from inside the tooltip's open handler which works in Firefox? Or is there some magic jQuery UI Tooltip option/method that can attain this desired behavior in a simpler/similar way?

Answer

Fabrício Matté picture Fabrício Matté · Apr 22, 2013

This is a rather hackish solution, but here goes the temporary fix which I just found. edit: after having finished the 1st cross-browser solution below, it is not so hackish at all. The #1, #4 and #2 solution listed below should be usable.

jQuery Event objects have a hidden originalEvent property, which in the question's case is a references a native mouseover event. Therefore event.originalEvent.target can be used for both Chrome and Firefox.

open: function(e, ui) {
  var el = e.originalEvent.target;
  if (el.offsetWidth === el.scrollWidth) {
    ui.tooltip.hide();
  }
}

Bin

When old IE support is concerned, you will have to use event.srcElement when event.target is not present.

var el = e.originalEvent.target || e.originalEvent.srcElement;

Bin


#1 Cross-browser solution

Finally, when there are nested elements inside of an element that triggers the tooltip, you will have to monkeypatch it by using the .closest() method passing in the same filter as your tooltip's delegation selector (items option, default to [title]:not([disabled]) as of UI 1.10.2):

var el = $(e.originalEvent.target || e.originalEvent.srcElement).closest($(this).tooltip('option', 'items'))[0];

Bin

This is basically what the Tooltip Widget does internally as shown here.


#2 Alternative cross-browser solution

Alternative solution that doesn't require so many workarounds by using a simple DOM query:

var el = $('[aria-describedby="'+ui.tooltip[0].id+'"]')[0];

Bin


#3 Internal methods abuse

This should not be used, but by overriding the internal _open method you get access to a target jQuery object which contains the event's target element passed as parameter to it. Problem is, then you don't have access to its tooltip widget not even through .tooltip('widget') as it is "not created" yet though already present in the DOM. You can work around it using the internal _find method which will perform a DOM query by ID and thus you can hide it right after the show animation would kick in, while its live cycle is not affected - it will be there and removed on mouseleave as usual, but will have display:none along all this cycle.

var bk_open = $.ui.tooltip.prototype._open;
$.ui.tooltip.prototype._open = function(event, target, content) {
  bk_open.apply(this, arguments);
  if (target[0].offsetWidth === target[0].scrollWidth) {
    this._find(target).hide();
  }
};

$(document).tooltip();

Bin

That DOM query with _find is rather unnecessary, so we can also extend the internal _tooltip method which returns a jQuery object containing the tooltip element so we can use JS's lexical scope to save a reference to the tooltip element before our overridden _open executes:

var tooltipproto = $.ui.tooltip.prototype,
    bk_open = tooltipproto._open,
    bk_tooltip = tooltipproto._tooltip,
    $tooltip;
tooltipproto._open = function(event, target, content) {
  bk_open.apply(this, arguments);
  if (target[0].offsetWidth === target[0].scrollWidth) {
    $tooltip.hide();
  }
};
tooltipproto._tooltip = function(element) {
  return ($tooltip = bk_tooltip.apply(this, arguments));
};
$(document).tooltip();

Bin

Of course, as the _tooltip internal methods receives the target as parameter and returns the tooltip, one could do the entire operation just overriding this method, but as the tooltip is shown after this method returns, this would require a setTimeout(fn, 0) possibly causing an undesirable flicker effect.

This is overly hackish, cumbersome and verbose for something so simple though.


#4 Clean solution

"Clean" as in not using undocumented methods nor attributes nor prototype overriding nor DOM queries. Back to the first snippet, all we needed was a reference to the element that triggered the tooltip. This element is referenced by the this inside of the content function which is called prior to the open handler, thus we can use lexical scope to store that reference a level above:

var el;
$(document).tooltip({
  content: function() {
    el = this;
    return this.title;
  },
  open: function(e, ui) {
    if (el.offsetWidth === el.scrollWidth) {
      ui.tooltip.hide();
    }
  }
});

Bin

Note that my custom content function in the snippet above removes jQuery's default HTML tags stripping (because I like using HTML inside the tooltips), but this may be an issue if you're dynamically populating the title attribute with user-inputted data, so in case you want to keep the original content handler's functionality:

var el,
    bk_content = $.ui.tooltip.prototype.options.content;
$(document).tooltip({
  content: function() {
    el = this;
    return bk_content.apply(this, arguments);
  },
  open: function(e, ui) {
    if (el.offsetWidth === el.scrollWidth) {
      ui.tooltip.hide();
    }
  }
});

Bin


I'll open a ticket on jQuery UI bugtracker requesting this feature to be implemented meanwhile. Here's it:

Tooltip: Expose element which triggered the tooltip inside open/close handlers