This project is archived and is in readonly mode.

#167 ✓resolved
Jari Berg

Tips class issues in More

Reported by Jari Berg | October 28th, 2009 @ 09:43 AM | in

I've found 2 issues with the Tips class in More

  1. fireForParent throws this error:

    "element.getParent is not a function"

The fix:

  • var element = $(element); if (!element) return;
  1. position function tries to position the tip element before it's visible, thus causing the tip to show up outside of the screen if having a tooltip on the screen edge.

The fix:

position: function(event){
    + if (this.tip.getStyle('display') != 'block') return;
    + this.tip.setStyle('visibility', 'visible');
  1. 'visibility' style is added to avoid the tip to show on calling "show" function. 'display: none' style is added to hide the tip container completely until 'show' is called.

    toElement: function() {
        styles: {
    + visibility: 'hidden',
    + display: 'none'

This fixes the 'flickering' when positioning the tip div container.

Comments and changes to this ticket

  • Aicke Schulz

    Aicke Schulz October 29th, 2009 @ 12:31 PM

    • Tag changed from tips to, fix, plugin, tips

    I think i've found the real problem in fireForParent(), the line

    else this.fireForParent(parentNode, event);

    should be

    else this.fireForParent(event, parentNode);

    The solution of the op kills the bubbling of the event (so the whole function becomes useless), imho

  • Jari Berg

    Jari Berg October 29th, 2009 @ 02:11 PM

    You're right about the else this.fireForParent part, but it still does not work. Since my last post, I have modified the Tips class. The following is working as intended (also in IE6):


    var read = function(option, element){

    return (option) ? ($type(option) == 'function' ? option(element) : element.get(option)) : '';


    this.Tips = new Class({

    Implements: [Events, Options],
    options: {
        onAttach: $empty(element),
        onDetach: $empty(element),
        onShow: function(){
            this.tip.setStyle('display', 'block');
        onHide: function(){
            this.tip.setStyle('display', 'none');
        title: 'title',
        text: function(element){
            return element.get('rel') || element.get('href');
        showDelay: 100,
        hideDelay: 100,
        className: 'tip-wrap',
        offset: {x: 16, y: 16},
        fixed: false
    initialize: function(){
        var params =, {options: Object.type, elements: $defined});
        if (params.elements) this.attach(params.elements);
    toElement: function(){
        if (this.tip) return this.tip;
        this.container = new Element('div', {'class': 'tip'});
        return this.tip = new Element('div', {
            'class': this.options.className,
            styles: {
                position: 'absolute',
                top: 0,
                left: 0,
                visibility: 'hidden',
                display: 'none'
            new Element('div', {'class': 'tip-top'}),
            new Element('div', {'class': 'tip-bottom'})
    attach: function(elements){
            var title = read(this.options.title, element),
                text = read(this.options.text, element);
            element.erase('title').store('tip:native', title).retrieve('tip:title', title);
            element.retrieve('tip:text', text);
            this.fireEvent('attach', [element]);
            var events = ['enter', 'leave'];
            if (!this.options.fixed) events.push('move');
                var event = element.retrieve('tip:' + value);
                if (!event) event = this['element' + value.capitalize()].bindWithEvent(this, element);
      'tip:' + value, event).addEvent('mouse' + value, event);
            }, this);
            element.addEvent('mouseout', this.hide.bind(this, element)); // firefox doesn't handle mouseleave correct
        }, this);
        return this;
    detach: function(elements){
            ['enter', 'leave', 'move'].each(function(value){
                element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value);
            this.fireEvent('detach', [element]);
            if (this.options.title == 'title'){ // This is necessary to check if we can revert the title
                var original = element.retrieve('tip:native');
                if (original) element.set('title', original);
        }, this);
        return this;
    elementEnter: function(event, element){
        ['title', 'text'].each(function(value){
            var content = element.retrieve('tip:' + value);
            if (content) this.fill(new Element('div', {'class': 'tip-' + value}).inject(this.container), content);
        }, this);
        this.timer =, this, [element, event]);
    elementLeave: function(event, element){
        this.tip.setStyle('visibility', 'hidden');
        this.timer = this.hide.delay(this.options.hideDelay, this, element);
        this.fireForParent(event, element);
    fireForParent: function(event, element){
        var element = $(element);
        if (!element) return;
        parentNode = element.getParent();
        if (parentNode == document.body) return;
        if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event);
        else this.fireForParent(event, parentNode);
    elementMove: function(event, element) {
    position: function(event){
        if (this.tip.getStyle('display') != 'block') return;
        var size = window.getSize(), scroll = window.getScroll(),
            tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight},
            props = {x: 'left', y: 'top'},
            obj = {};
        for (var z in props){
            obj[props[z]] =[z] + this.options.offset[z];
            if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] =[z] - this.options.offset[z] - tip[z];
        this.tip.setStyle('visibility', 'visible');
    fill: function(element, contents){
        if(typeof contents == 'string') element.set('html', contents);
        else element.adopt(contents);
    show: function(element, event) {
        this.fireEvent('show', [this.tip, element]);
        this.position((this.options.fixed) ? {page: element.getPosition()} : event);
    hide: function(element){
        this.fireEvent('hide', [this.tip, element]);



  • Fábio M. Costa

    Fábio M. Costa November 4th, 2009 @ 01:15 PM

    • Assigned user set to “Aaron Newton”

    Aicke Schulz gave the correct patch. Simple like that.

  • Jari Berg

    Jari Berg November 4th, 2009 @ 02:08 PM

    I'm sorry to disappoint you, but simply replacing "this.fireForParent(parentNode, event)" with "this.fireForParent(event, parentNode)" does not fix the issues. Try it in Firefox 3.5, IE6 and you should know.

  • Jason Beaudoin

    Jason Beaudoin November 4th, 2009 @ 05:53 PM

    I implemented both suggestions (Aicke Schulz + Jari Berg) and the plugin isn't causing anymore errors. Using Firefox 3.5.4. Haven't tried IE6.

  • TTocco

    TTocco November 4th, 2009 @ 10:39 PM

    I've run into the same issue while trying to implement html tips so I took these code examples and tested them out.

    When using html tips there is something amiss with the enter and leave events. I'm inserting a static

    with an in the tip and while mousing around inside the containing element it behaves erratically when crossing the boundaries of the image but remaining inside the tip container where the enter and leave are supposed to be bound. Cursor motion also seems to cause the tip to lag a bit.

    Moo-more does not exhibit these behaviors when I back up to that version while keeping everything else the same.

    On a side note concerning Tips.js does it make more sense to adopt the target into the tip element than it does to copy the defined element's HTML ( element.get('html') )? I ran into an issue today where I was dynamically replacing the contents of the tip using Storage and it was adopting static html tips into the tip container then junking them when I stored the next value.

    As an example assume you have a frame and multiple tips. The frame contains an image which you will switch out based upon form activity. When the image changes the tip also needs to change to match the image.

    <div style="height:100px;width:100px" id="imageframe">
        <img src="hoveroverme.jpg" /> <!-- this image and associated tip is dynamic -->
    <div style="display:none">
        <div id="tip1"> <img src="whatever1.jpg" /> </div>
        <div id="tip2"> <img src="whatever2.jpg" /> </div>
        <div id="tip3"> <img src="whatever3.jpg" /> </div>
        <div id="tip4"> <img src="whatever4.jpg" /> </div>

    Assume you have bound Tips to "imageframe" and begin storing your tips in this order.
'imageframe').store('tip:text', $('tip1'));'imageframe').store('tip:text', $('tip2'));'imageframe').store('tip:text', $('tip3'));'imageframe').store('tip:text', $('tip4'));

    Doing this will destroy tip1-tip3 from the DOM preventing me from using it again. Here's what I've thrown together to work around the implementation but I wonder if copying isn't a better default due to the nature of tips in general.

    var Tips2 = new Class({
        Extends: Tips,
        fill: function(element, contents){
            if(typeof contents == 'string') element.set('html', contents);
            //else element.adopt(contents);
            else element.set('html', contents.get('html'))
  • Aaron Newton

    Aaron Newton November 17th, 2009 @ 11:06 PM

    • State changed from “new” to “resolved”
    • Milestone set to

    All the bugs with the parent logic have been fixed, so I'm closing this bug. As for the extended use case that TTocco posted, that's outside of the requirements of what Tips does. If you need a more complex Tips class, extend it.

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

The MooTools Extensions