YUI 3.x Home -

YUI Library Examples: Widget: Creating a simple Tooltip widget

Widget: Creating a simple Tooltip widget

This is an advanced example, in which we create a Tooltip widget, by extending the base Widget class, and adding WidgetStack and WidgetPosition extensions, through Base.build.
Tooltip One (content from title)
Tooltip Two (content set in event listener)
Tooltip Three (content from lookup)
Tooltip Four (content from title)

Creating A Tooltip Widget Class

Basic Class Structure

As with the basic "Extending Widget" example, the Tooltip class will extend the Widget base class and follows the same pattern we use for other classes which extend Base.

Namely:

  • Set up the constructor to invoke the superclass constructor
  • Define a NAME property, to identify the class
  • Define the default attribute configuration, using the ATTRS property
  • Implement prototype methods

This basic structure is shown below:

  1. /*
  2.  * Required NAME static field, used to identify the Widget class and
  3.  * used as an event prefix, to generate class names etc. (set to the
  4.  * class name in camel case).
  5.  */
  6. Tooltip.NAME = "tooltip";
  7.  
  8. /* Default Tooltip Attributes */
  9. Tooltip.ATTRS = {
  10.  
  11. /*
  12.   * The tooltip content. This can either be a fixed content value,
  13.   * or a map of id-to-values, designed to be used when a single
  14.   * tooltip is mapped to multiple trigger elements.
  15.   */
  16. content : {
  17. value: null
  18. },
  19.  
  20. /*
  21.   * The set of nodes to bind to the tooltip instance. Can be a string,
  22.   * or a node instance.
  23.   */
  24. triggerNodes : {
  25. value: null,
  26. setter: function(val) {
  27. if (val && Lang.isString(val)) {
  28. val = Node.all(val);
  29. }
  30. return val;
  31. }
  32. },
  33.  
  34. /*
  35.   * The delegate node to which event listeners should be attached.
  36.   * This node should be an ancestor of all trigger nodes bound
  37.   * to the instance. By default the document is used.
  38.   */
  39. delegate : {
  40. value: null,
  41. setter: function(val) {
  42. return Y.one(val) || Y.one("document");
  43. }
  44. },
  45.  
  46. /*
  47.   * The time to wait, after the mouse enters the trigger node,
  48.   * to display the tooltip
  49.   */
  50. showDelay : {
  51. value:250
  52. },
  53.  
  54. /*
  55.   * The time to wait, after the mouse leaves the trigger node,
  56.   * to hide the tooltip
  57.   */
  58. hideDelay : {
  59. value:10
  60. },
  61.  
  62. /*
  63.   * The time to wait, after the tooltip is first displayed for
  64.   * a trigger node, to hide it, if the mouse has not left the
  65.   * trigger node
  66.   */
  67. autoHideDelay : {
  68. value:2000
  69. },
  70.  
  71. /*
  72.   * Override the default visibility set by the widget base class
  73.   */
  74. visible : {
  75. value:false
  76. },
  77.  
  78. /*
  79.   * Override the default XY value set by the widget base class,
  80.   * to position the tooltip offscreen
  81.   */
  82. xy: {
  83. value:[Tooltip.OFFSCREEN_X, Tooltip.OFFSCREEN_Y]
  84. }
  85. };
  86.  
  87. Y.extend(Tooltip, Y.Widget, {
  88. // Prototype methods/properties
  89. });
/* 
 *  Required NAME static field, used to identify the Widget class and 
 *  used as an event prefix, to generate class names etc. (set to the 
 *  class name in camel case). 
 */
Tooltip.NAME = "tooltip";
 
/* Default Tooltip Attributes */
Tooltip.ATTRS = {
 
    /* 
     * The tooltip content. This can either be a fixed content value, 
     * or a map of id-to-values, designed to be used when a single
     * tooltip is mapped to multiple trigger elements.
     */
    content : {
        value: null
    },
 
    /* 
     * The set of nodes to bind to the tooltip instance. Can be a string, 
     * or a node instance.
     */
    triggerNodes : {
        value: null,
        setter: function(val) {
            if (val && Lang.isString(val)) {
                val = Node.all(val);
            }
            return val;
        }
    },
 
    /*
     * The delegate node to which event listeners should be attached.
     * This node should be an ancestor of all trigger nodes bound
     * to the instance. By default the document is used.
     */
    delegate : {
        value: null,
        setter: function(val) {
            return Y.one(val) || Y.one("document");
        }
    },
 
    /*
     * The time to wait, after the mouse enters the trigger node,
     * to display the tooltip
     */
    showDelay : {
        value:250
    },
 
    /*
     * The time to wait, after the mouse leaves the trigger node,
     * to hide the tooltip
     */
    hideDelay : {
        value:10
    },
 
    /*
     * The time to wait, after the tooltip is first displayed for 
     * a trigger node, to hide it, if the mouse has not left the 
     * trigger node
     */
    autoHideDelay : {
        value:2000
    },
 
    /*
     * Override the default visibility set by the widget base class
     */
    visible : {
        value:false
    },
 
    /*
     * Override the default XY value set by the widget base class,
     * to position the tooltip offscreen
     */
    xy: {
        value:[Tooltip.OFFSCREEN_X, Tooltip.OFFSCREEN_Y]
    }
};
 
Y.extend(Tooltip, Y.Widget, { 
    // Prototype methods/properties
});

Adding WidgetPosition and WidgetStack Extension Support

The Tooltip class also needs basic positioning and stacking (z-index, shimming) support. As with the Custom Widget Classes example, we use Base.build to add this support to the Tooltip class. However in this case, we want to modify the Tooltip class, as opposed to extending it to create a completely new class, hence we set the dynamic configuration property to false:

  1. // dynamic:false = Modify the existing Tooltip class
  2. Tooltip = Y.Base.build("tooltip", Tooltip, [Y.WidgetPosition, Y.WidgetStack], {dynamic:false});
// dynamic:false = Modify the existing Tooltip class
Tooltip = Y.Base.build("tooltip", Tooltip, [Y.WidgetPosition, Y.WidgetStack], {dynamic:false});

Lifecycle Methods: initializer, destructor

The initializer method is invoked during the init lifecycle phase, after the attributes are configured for each class. Tooltip uses it to setup the private state variables it will use to store the trigger node currently being serviced by the tooltip instance, event handles and show/hide timers.

  1. initializer : function(config) {
  2.  
  3. this._triggerClassName = this.getClassName("trigger");
  4.  
  5. // Currently bound trigger node information
  6. this._currTrigger = {
  7. node: null,
  8. title: null,
  9. mouseX: Tooltip.OFFSCREEN_X,
  10. mouseY: Tooltip.OFFSCREEN_Y
  11. };
  12.  
  13. // Event handles - mouse over is set on the delegate
  14. // element, mousemove and mouseout are set on the trigger node
  15. this._eventHandles = {
  16. delegate: null,
  17. trigger: {
  18. mouseMove : null,
  19. mouseOut: null
  20. }
  21. };
  22.  
  23. // Show/hide timers
  24. this._timers = {
  25. show: null,
  26. hide: null
  27. };
  28.  
  29. // Publish events introduced by Tooltip. Note the triggerEnter event is preventable,
  30. // with the default behavior defined in the _defTriggerEnterFn method
  31. this.publish("triggerEnter", {defaultFn: this._defTriggerEnterFn, preventable:true});
  32. this.publish("triggerLeave", {preventable:false});
  33. }
initializer : function(config) {
 
    this._triggerClassName = this.getClassName("trigger");
 
    // Currently bound trigger node information
    this._currTrigger = {
        node: null,
        title: null,
        mouseX: Tooltip.OFFSCREEN_X,
        mouseY: Tooltip.OFFSCREEN_Y
    };
 
    // Event handles - mouse over is set on the delegate
    // element, mousemove and mouseout are set on the trigger node
    this._eventHandles = {
        delegate: null,
        trigger: {
            mouseMove : null,
            mouseOut: null
        }
    };
 
    // Show/hide timers
    this._timers = {
        show: null,
        hide: null
    };
 
    // Publish events introduced by Tooltip. Note the triggerEnter event is preventable,
    // with the default behavior defined in the _defTriggerEnterFn method 
    this.publish("triggerEnter", {defaultFn: this._defTriggerEnterFn, preventable:true});
    this.publish("triggerLeave", {preventable:false});
}

The destructor is used to clear out stored state, detach any event handles and clear out the show/hide timers:

  1. destructor : function() {
  2. this._clearCurrentTrigger();
  3. this._clearTimers();
  4. this._clearHandles();
  5. }
destructor : function() {
    this._clearCurrentTrigger();
    this._clearTimers();
    this._clearHandles();
}

Lifecycle Methods: bindUI, syncUI

The bindUI and syncUI are invoked by the base Widget class' renderer method.

bindUI is used to bind the attribute change listeners used to update the rendered UI from the current state of the widget and also to bind the DOM listeners required to enable the UI for interaction.

syncUI is used to sync the UI state from the current widget state, when initially rendered.

NOTE: Widget's renderer method also invokes the renderUI method, which is responsible for laying down any additional content elements a widget requires. However tooltip does not have any additional elements in needs to add to the DOM, outside of the default Widget boundingBox and contentBox.

  1. bindUI : function() {
  2. this.after("delegateChange", this._afterSetDelegate);
  3. this.after("nodesChange", this._afterSetNodes);
  4.  
  5. this._bindDelegate();
  6. },
  7.  
  8. syncUI : function() {
  9. this._uiSetNodes(this.get("triggerNodes"));
  10. }
bindUI : function() {
    this.after("delegateChange", this._afterSetDelegate);
    this.after("nodesChange", this._afterSetNodes);
 
    this._bindDelegate();
},
 
syncUI : function() {
    this._uiSetNodes(this.get("triggerNodes"));
}

Attribute Supporting Methods

Tooltip's triggerNodes, which defines the set of nodes which should trigger this tooltip instance, has a couple of supporting methods associated with it.

The _afterSetNodes method is the default attribute change event handler for the triggerNodes attribute. It invokes the _uiSetNodes method, which marks all trigger nodes with a trigger class name (yui-tooltip-trigger) when set.

  1. _afterSetNodes : function(e) {
  2. this._uiSetNodes(e.newVal);
  3. },
  4.  
  5. _uiSetNodes : function(nodes) {
  6. if (this._triggerNodes) {
  7. this._triggerNodes.removeClass(this._triggerClassName);
  8. }
  9.  
  10. if (nodes) {
  11. this._triggerNodes = nodes;
  12. this._triggerNodes.addClass(this._triggerClassName);
  13. }
  14. },
_afterSetNodes : function(e) {
    this._uiSetNodes(e.newVal);
},
 
_uiSetNodes : function(nodes) {
    if (this._triggerNodes) {
        this._triggerNodes.removeClass(this._triggerClassName);
    }
 
    if (nodes) {
        this._triggerNodes = nodes;
        this._triggerNodes.addClass(this._triggerClassName);
    }
},

Similarly the _afterSetDelegate method is the default attributechange listener for the delegate attribute, and invokes _bindDelegate to set up the listeners when a new delegate node is set.

  1. _afterSetDelegate : function(e) {
  2. this._bindDelegate(e.newVal);
  3. },
  4.  
  5. _bindDelegate : function() {
  6. var eventHandles = this._eventHandles;
  7.  
  8. if (eventHandles.delegate) {
  9. eventHandles.delegate.detach();
  10. eventHandles.delegate = null;
  11. }
  12. eventHandles.delegate = Y.on("mouseover", Y.bind(this._onDelegateMouseOver, this), this.get("delegate"));
  13. },
_afterSetDelegate : function(e) {
    this._bindDelegate(e.newVal);
},
 
_bindDelegate : function() {
    var eventHandles = this._eventHandles;
 
    if (eventHandles.delegate) {
        eventHandles.delegate.detach();
        eventHandles.delegate = null;
    }
    eventHandles.delegate = Y.on("mouseover", Y.bind(this._onDelegateMouseOver, this), this.get("delegate"));
},

DOM Event Handlers

Tooltips interaction revolves around the mouseover, mousemove and mouseout DOM events. The mouseover listener is the only listener set up initially, on the delegate node:

  1. _onDelegateMouseOver : function(e) {
  2. var node = this.getParentTrigger(e.target);
  3. if (node && (!this._currTrigger.node || !node.compareTo(this._currTrigger.node))) {
  4. this._enterTrigger(node, e.pageX, e.pageY);
  5. }
  6. }
_onDelegateMouseOver : function(e) {
    var node = this.getParentTrigger(e.target);
    if (node && (!this._currTrigger.node || !node.compareTo(this._currTrigger.node))) {
        this._enterTrigger(node, e.pageX, e.pageY);
    }
}

It attempts to determine if the mouse is entering a trigger node. It ignores mouseover events generated from elements inside the current trigger node (for example when mousing out of a child element of a trigger node). If it determines that the mouse is entering a trigger node, the delegates to the _enterTrigger method to setup the current trigger state and attaches mousemove and mouseout listeners on the current trigger node.

The mouse out listener delegates to the _leaveTrigger method, if it determines the mouse is leaving the trigger node:

  1. _onNodeMouseOut : function(e) {
  2. var to = e.relatedTarget;
  3. var trigger = e.currentTarget;
  4.  
  5. if (!trigger.contains(to)) {
  6. this._leaveTrigger(trigger);
  7. }
  8. }
_onNodeMouseOut : function(e) {
    var to = e.relatedTarget;
    var trigger = e.currentTarget;
 
    if (!trigger.contains(to)) {
        this._leaveTrigger(trigger);
    }
}

The mouse move listener delegates to the _overTrigger method to store the current mouse XY co-ordinates (used to position the Tooltip when it is displayed after the showDelay):

  1. _onNodeMouseMove : function(e) {
  2. this._overTrigger(e.pageX, e.pageY);
  3. }
_onNodeMouseMove : function(e) {
    this._overTrigger(e.pageX, e.pageY);
}

Trigger Event Delegates: _enterTrigger, _leaveTrigger, _overTrigger

As seen above, the DOM event handlers delegate to the _enterTrigger, _leaveTrigger and _overTrigger methods to update the Tooltip state based on the currently active trigger node.

The _enterTrigger method sets the current trigger state (which node is the current tooltip trigger, what the current mouse XY position is, etc.). The method also fires the triggerEnter event, whose default function actually handles showing the tooltip after the configured showDelay period. The triggerEnter event can be prevented by listeners, allowing users to prevent the tooltip from being shown if required. (triggerEnter listeners are passed the current trigger node and pageX, pageY mouse co-ordinates as event facade properties):

  1. _enterTrigger : function(node, x, y) {
  2. this._setCurrentTrigger(node, x, y);
  3. this.fire("triggerEnter", null, node, x, y);
  4. },
  5.  
  6. _defTriggerEnterFn : function(e) {
  7. var node = e.node;
  8. if (!this.get("disabled")) {
  9. this._clearTimers();
  10. var delay = (this.get("visible")) ? 0 : this.get("showDelay");
  11. this._timers.show = Y.later(delay, this, this._showTooltip, [node]);
  12. }
  13. },
_enterTrigger : function(node, x, y) {
    this._setCurrentTrigger(node, x, y);
    this.fire("triggerEnter", null, node, x, y);
},
 
_defTriggerEnterFn : function(e) {
    var node = e.node;
    if (!this.get("disabled")) {
        this._clearTimers();
        var delay = (this.get("visible")) ? 0 : this.get("showDelay");
        this._timers.show = Y.later(delay, this, this._showTooltip, [node]);
    }
},

Similarly the _leaveTrigger method is invoked when the mouse leaves a trigger node, and clears any stored state, timers and listeners before setting up the hideDelay timer. It fires a triggerLeave event, but cannot be prevented, and has no default behavior to prevent:

  1. _leaveTrigger : function(node) {
  2. this.fire("triggerLeave");
  3.  
  4. this._clearCurrentTrigger();
  5. this._clearTimers();
  6.  
  7. this._timers.hide = Y.later(this.get("hideDelay"), this, this._hideTooltip);
  8. },
_leaveTrigger : function(node) {
    this.fire("triggerLeave");
 
    this._clearCurrentTrigger();
    this._clearTimers();
 
    this._timers.hide = Y.later(this.get("hideDelay"), this, this._hideTooltip);
},

As mentioned previously, the _overTrigger method simply stores the current mouse XY co-ordinates for use when the tooltip is shown:

  1. _overTrigger : function(x, y) {
  2. this._currTrigger.mouseX = x;
  3. this._currTrigger.mouseY = y;
  4. }
_overTrigger : function(x, y) {
    this._currTrigger.mouseX = x;
    this._currTrigger.mouseY = y;
}

Setting Tooltip Content

Since the content for a tooltip is usually a function of the trigger node and not constant, Tooltip provides a number of ways to set the content.

  1. Setting the content attribute to a string or node. In this case, the value of the content attribute is used for all triggerNodes
  2. Setting the content attribute to an object literal, containing a map of triggerNode id to content. The content for a trigger node will be set using the map, when the tooltip is triggered for the node.
  3. Setting the title attribute on the trigger node. The value of the title attribute is used to set the tooltip content, when triggered for the node.
  4. By calling the setTriggerContent method to set content for a specific trigger node, in a triggerEnter event listener.

The precedence of these methods is handled in the _setTriggerContent method, invoked when the mouse enters a trigger:

  1. _setTriggerContent : function(node) {
  2. var content = this.get("content");
  3. if (content && !(content instanceof Node || Lang.isString(content))) {
  4. content = content[node.get("id")] || node.getAttribute("title");
  5. }
  6. this.setTriggerContent(content);
  7. },
  8.  
  9. setTriggerContent : function(content) {
  10. var contentBox = this.get("contentBox");
  11. contentBox.set("innerHTML", "");
  12.  
  13. if (content) {
  14. if (content instanceof Node) {
  15. for (var i = 0, l = content.size(); i < l; ++i) {
  16. contentBox.appendChild(content.item(i));
  17. }
  18. } else if (Lang.isString(content)) {
  19. contentBox.set("innerHTML", content);
  20. }
  21. }
  22. }
_setTriggerContent : function(node) {
    var content = this.get("content");
    if (content && !(content instanceof Node || Lang.isString(content))) {
        content = content[node.get("id")] || node.getAttribute("title");
    }
    this.setTriggerContent(content);
},
 
setTriggerContent : function(content) {
    var contentBox = this.get("contentBox");
    contentBox.set("innerHTML", "");
 
    if (content) {
        if (content instanceof Node) {
            for (var i = 0, l = content.size(); i < l; ++i) {
                contentBox.appendChild(content.item(i));
            }
        } else if (Lang.isString(content)) {
            contentBox.set("innerHTML", content);
        }
    }
}

Calling the public setTriggerContent in a triggerEvent listener will over-ride content set using the content attribute or the trigger node's title value.

Using Tooltip

For this example, we set up 4 DIV elements which will act as tooltip triggers. They are all marked using a yui-hastooltip class, so that they can be queried using a simple selector, passed as the value for the triggerNodes attribute in the tooltip's constructor Also all 4 trigger nodes are contained in a wrapper DIV with id="delegate" which will act as the delegate node.

  1. var tt = new Tooltip({
  2. triggerNodes:".yui-hastooltip",
  3. delegate: "#delegate",
  4. content: {
  5. tt3: "Tooltip 3 (from lookup)"
  6. },
  7. shim:false,
  8. zIndex:2
  9. });
  10. tt.render();
var tt = new Tooltip({
    triggerNodes:".yui-hastooltip",
    delegate: "#delegate",
    content: {
        tt3: "Tooltip 3 (from lookup)"
    },
    shim:false,
    zIndex:2
});
tt.render();

The tooltip content for each of the trigger nodes is setup differently. The first trigger node uses the title attribute to set it's content. The third trigger node's content is set using the content map set in the constructor above. The second trigger node's content is set using a triggerEnter event listener and the setTriggerContent method as shown below:

  1. tt.on("triggerEnter", function(e) {
  2. var node = e.node;
  3. if (node && node.get("id") == "tt2") {
  4. this.setTriggerContent("Tooltip 2 (from triggerEvent)");
  5. }
  6. });
tt.on("triggerEnter", function(e) {
    var node = e.node;
    if (node && node.get("id") == "tt2") {
        this.setTriggerContent("Tooltip 2 (from triggerEvent)");
    }
});

The fourth trigger node's content is set using it's title attribute, however it also has a triggerEvent listener which prevents the tooltip from being displayed for it, if the checkbox is checked.

  1. var prevent = Y.one("#prevent");
  2. tt.on("triggerEnter", function(e) {
  3. var node = e.node;
  4. if (prevent.get("checked")) {
  5. if (node && node.get("id") == "tt4") {
  6. e.preventDefault();
  7. }
  8. }
  9. });
var prevent = Y.one("#prevent");
tt.on("triggerEnter", function(e) {
    var node = e.node;
    if (prevent.get("checked")) {
        if (node && node.get("id") == "tt4") {
            e.preventDefault();
        }
    }
});

Copyright © 2009 Yahoo! Inc. All rights reserved.

Privacy Policy - Terms of Service - Copyright Policy - Job Openings