Search code examples
javascripteventsmootools

mootools | Open/close popup menu and outer click event


I'm using mootools and working on popup menu:

document.getElement('.cart a').toggle(
  function() {
   this.getParent('div').removeClass('open');
   this.getNext('.cart_contents').hide();               
  },
  function() {
    this.getParent('div').addClass('open');
    this.getNext('.cart_contents').show();
  })
);

The toggle function implementation:

Element.implement({
    toggle: function(fn1,fn2){
        this.store('toggled',false);
        return this.addEvent('click',function(event){
            event.stop();
             if(this.retrieve('toggled')){
                 fn1.call(this);
             }else{
                 fn2.call(this);
             }
            this.store('toggled',!(this.retrieve('toggled')));
        });
    }
});

The outer click function:

Element.Events.outerClick = {
    base : 'click',    
    condition : function(event){
        event.stopPropagation();
        return false;
    },
    onAdd : function(fn){
        this.getDocument().addEvent('click', fn);
    },
    onRemove : function(fn){
        this.getDocument().removeEvent('click', fn);
    }
};

I would like to add outerclick event to close my popup menu:

document.getElement('.cart a').toggle(
      function() {
       this.getParent('div').removeClass('open');
       this.getNext('.cart_contents').hide();               
      },
      function() {
        this.getParent('div').addClass('open');
        this.getNext('.cart_contents').show();
      }).addEvent('outerClick',function() {
        // Error: Wrong code below 
        // Uncaught TypeError: Object #<HTMLDocument> has no method 'getParent' 
        this.getParent('div').removeClass('open');
        this.getNext('.cart_contents').hide();  
});

Error: Uncaught TypeError: Object # has no method 'getParent' Thanks.


Solution

  • This is a problem (or a deficiency) to do with the outerClick implementation by Darren. It's not a bug - it's built to work as fast as possible. you just need to understand what it does when it binds the actual event to document.

    Element.Events.outerClick = {
        base : 'click',    
        condition : function(event){
            event.stopPropagation();
            return false;
        },
        onAdd : function(fn){
            // the event actually gets added to document!
            // hence scope in fn will be document as delegator.
            this.getDocument().addEvent('click', fn);
        },
        onRemove : function(fn){
            this.getDocument().removeEvent('click', fn);
        }
    };
    

    So the functions will run with context this === document.

    One way to fix it is to bind the callback specifically to the element. Problem is, removing it won't work as .bind will return a unique new function that won't match the same function again.

    (function(){
        var Element = this.Element,
            Elements = this.Elements;
    
        [Element, Elements].invoke('implement', {
            toggle: function(){
                var args = Array.prototype.slice.call(arguments), 
                    count = args.length-1,
                    // start at 0
                    index = 0;
    
                return this.addEvent('click', function(){
                    var fn = args[index];
                    typeof fn === 'function' && fn.apply(this, arguments);
                    // loop args.
                    index = count > index ? index+1 : 0;
                });
            }
        });
    
        Element.Events.outerClick = {
            base : 'click',    
            condition : function(event){
                event.stopPropagation();
                return false;
            },
            onAdd : function(fn){
                this.getDocument().addEvent('click', fn.bind(this));
            },
            onRemove : function(fn){
                // WARNING: fn.bind(this) !== fn.bind(this) so the following
                // will not work. you need to keep track of bound fns or 
                // do it upstream before you add the event.
                this.getDocument().removeEvent('click', fn.bind(this));
            }
        };
    }());
    
    document.id('myp').toggle(
        function(e){
            console.log(e); // event.
            this.set('html', 'new text');
        },
        function(){
            console.log(this); // element
            this.set('html', 'old text');
        },
        function(){
            this.set("html", "function 3!");
        }
    ).addEvent('outerClick', function(e){
         console.log(this, e); 
    });
    

    http://jsfiddle.net/dimitar/UZRx5/ - this will work for now - depends if you have a destructor that removes it.

    Another approach is when you add the event to do so:

    var el = document.getElement('.cart a');
    el.addEvent('outerClick', function(){ el.getParent(); // etc });
    // or bind it 
    el.addEvent('outerClick', function(){ this.getParent(); }.bind(el));
    

    it can then be removes if you save a ref

    var el = document.getElement('.cart a'),
        bound = function(){
            this.getParent('div');
        }.bind(el);
    
    el.addEvent('outerClick', bound);
    
    // later
    el.removeEvent('outerClick', bound);
    

    that's about it. an alternative outerClick is here: https://github.com/yearofmoo/MooTools-Event.outerClick/blob/master/Source/Event.outerClick.js - not tried it but looks like it tries to do the right thing by changing scope to element and keeping a reference of the memoized functions - though multiple events will likely cause an issue, needs event ids to identify the precise function to remove. Also, it looks quite heavy - considering the event is on document, you want to NOT do too much logic on each click that bubbles to that.