Thursday, June 30, 2011

Global tooltips with PrimeFaces and qTip2

PrimeFaces uses jQuery qTip plugin in the version 1 for global tooltips which are applied to all HTML elements having title attribute. There is an issue with ajax updates - global tooltips with qTip1 are gone after an ajax update. An ajax update of any element having the title attribute causes disappearance of nice tooltip. Native tooltips are still here but they are very plain, not beautiful and not themes aware. The official statement from the PrimeFaces team - "known issue, won't be fixed for global ones at the moment". In this post you will see how to resolve this issue in the simple way. The first step is to upgrade the qTip plugin to qTip2 and write a component class. The component class extends PrimeFaces' one and overwrites all methods what tooltip's default values have been changed for.
@ResourceDependencies({
    @ResourceDependency(library="primefaces", name="jquery/jquery.js"),
    @ResourceDependency(library="primefaces", name="core/core.js"),
    @ResourceDependency(library = "css", name = "jquery.qtip.css"),
    @ResourceDependency(library = "js", name = "tooltip/jquery.qtip.js"),
    @ResourceDependency(library = "js", name = "tooltip/tooltip.js")
})
public class Tooltip extends org.primefaces.component.tooltip.Tooltip
{
    public java.lang.String getTargetPosition() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.targetPosition, "bottom right");
    }

    public java.lang.String getPosition() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.position, "top left");
    }

    public java.lang.String getShowEffect() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.showEffect, "fadeIn");
    }

    public java.lang.String getHideEffect() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.hideEffect, "fadeOut");
    }

    public java.lang.String getShowEvent() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.showEvent, "mouseenter");
    }

    public java.lang.String getHideEvent() {
        return (java.lang.String) getStateHelper().eval(PropertyKeys.hideEvent, "mouseleave");
    }    
}
jquery.qtip.js is a new qTip script placed under the folder webapp/resources/js/tooltip and the tooltip.js is our one. In the tooltip.js I use the jQuery .live() method playing together with qTip2. This method allows us to easily add qTips to certain elements on a page, even when updating the DOM and adding new elements.
PrimeFaces.widget.Tooltip = function(cfg) {
    this.cfg = cfg;
    var _self = this;

    if (this.cfg.global) {
        // Bind the qTip within the event handler
        jQuery('*[title]').live(this.cfg.show.event, function(event) {
            var extCfg = _self.cfg;
            // Show the tooltip as soon as it's bound
            extCfg.show.ready = true;
            jQuery(this).qtip(extCfg, event);
        });
    } else {
        jQuery(PrimeFaces.escapeClientId(this.cfg.forComponent)).qtip(this.cfg);
    }
}

// make tooltip theme aware
jQuery.fn.qtip.defaults.style.widget = true;
jQuery.fn.qtip.defaults.style.classes = "ui-tooltip-rounded ui-tooltip-shadow";
jQuery ThemeRoller integration is done by the last two lines. The last step is to extend PrimeFaces's TooltipRenderer (because tooltip API has been changed) and register then all stuff in the faces-config.xml
public class TooltipRenderer extends org.primefaces.component.tooltip.TooltipRenderer
{
    public void encodeEnd(FacesContext facesContext, UIComponent component) throws IOException {
        Tooltip tooltip = (Tooltip) component;

        // dummy markup for ajax update (not really necessary)
        ResponseWriter writer = facesContext.getResponseWriter();
        writer.startElement("span", tooltip);
        writer.writeAttribute("id", tooltip.getClientId(facesContext), "id");
        writer.endElement("span");

        encodeScript(facesContext, tooltip);
    }

    protected void encodeScript(FacesContext facesContext, org.primefaces.component.tooltip.Tooltip tp)
        throws IOException {
        Tooltip tooltip = (Tooltip) tp;
        ResponseWriter writer = facesContext.getResponseWriter();
        boolean global = tooltip.isGlobal();
        String owner = getTarget(facesContext, tooltip);

        writer.startElement("script", null);
        writer.writeAttribute("type", "text/javascript", null);

        writer.write("jQuery(function() {");
        writer.write(tooltip.resolveWidgetVar() + " = new PrimeFaces.widget.Tooltip({");
        writer.write("global:" + global);

        if (!global) {
            writer.write(",forComponent:'" + owner + "'");
            writer.write(",content:'");
            if (tooltip.getValue() == null) {
                renderChildren(facesContext, tooltip);
            } else {
                writer.write(ComponentUtils.getStringValueToRender(facesContext, tooltip).replaceAll("'", "\\\\'"));
            }
            writer.write("'");
        }

        writer.write(",show:{event:'" + tooltip.getShowEvent() + "',delay:" + tooltip.getShowDelay() + 
            ",effect:function(){jQuery(this)." + tooltip.getShowEffect() + "(" + tooltip.getShowEffectLength() + ");}}");
        writer.write(",hide:{event:'" + tooltip.getHideEvent() + "',delay:" + tooltip.getHideDelay() + 
            ",effect:function(){jQuery(this)." + tooltip.getHideEffect() + "(" + tooltip.getHideEffectLength() + ");}}");
        writer.write(",position: {");
        String container = owner == null ? 
            "jQuery(document.body)" : "jQuery(PrimeFaces.escapeClientId('" + owner + "')).parent()";
        writer.write("container:" + container);
        writer.write(",at:'" + tooltip.getTargetPosition() + "'");
        writer.write(",my:'" + tooltip.getPosition() + "'");
        writer.write("}});});");

        writer.endElement("script");
    }
}
Have much fun! In one of my next post I will explain how to show a nice tooltip attached to the PrimeFaces Autocomplete component if user input disallowed characters.

Edit: I think a binding with namespace would be better to avoid collisions. Furthermore, a call of .die() before .live() is necessary in order to prevent miltiply binding of the same event if the tooltip gets updated via ajax. The same is for non global tooltips
jQuery('*[title]').die(this.cfg.show.event + ".tooltip").live(this.cfg.show.event + ".tooltip", ...
    ...
});

... in else ...

// delete previous tooltip to support ajax updates and create a new one
jQuery(PrimeFaces.escapeClientId(this.cfg.forComponent)).qtip('destroy').qtip(this.cfg);
The container element which the tooltip is appended to should be always document.body in order to play nice with layout, datatable, etc. Changes in TooltipRenderer.java:
writer.write("container:jQuery(document.body)");

No comments:

Post a Comment