Skip to main content

Bug Tracker

Side navigation

Ticket #4599: delegate.js


File delegate.js, 14.4 KB (added by justinbmeyer, April 28, 2009 06:08AM UTC)

Delegation plugin for jQuery

/**
 * @constructor
 * Attaches listeners for delegated events.
 * @init Creates a new delegator listener
 * @param {String} selector a css selector
 * @param {String} event a dom event
 * @param {Function} f a function to call
 */
jQuery.fn.delegate = function(selector, event, callback) {
  return this.each(function(){
    new jQuery.Delegator(selector, event, callback, this);
  });
};

jQuery.fn.kill = function(selector, event, callback) {
  return this.each(function(){
    //go through delegates remove delegate
    var delegates = jQuery.data(this, "delegates")[event];
    var i =0;
    while(i < delegates.length){
        if(delegates[i]._func == callback){
            delegates[i].destroy();
            delegates.splice(i, 1)
        }
        else
            i++;
    }
  });
};

jQuery.Delegator = function(selector, event, f, element){
        this._event = event;
        this._selector = selector
        this._func = f;
        this.element = element || document.documentElement;
        if(! jQuery.data(this.element, "delegateEvents") ) jQuery.data(this.element, "delegateEvents",{})
        
        //check if custom

        
        if(event == 'contextmenu' && jQuery.browser.opera) this.context_for_opera();
        else if(event == 'submit' && jQuery.browser.msie) this.submit_for_ie();
    	else if(event == 'change' && jQuery.browser.msie) this.change_for_ie();
    	else if(event == 'change' && jQuery.browser.safari) this.change_for_webkit();
    	else this.add_to_delegator();
        var delegates = jQuery.data(this.element, "delegates") || jQuery.data(this.element, "delegates",{})
        if(!delegates[event]){
            delegates[event]=[]
		}
		delegates[event].push(this);
}

jQuery.extend(jQuery.Delegator,
{
    /**
     * Adds kill() on an event.
     * @param {Object} event
     */
    addStopDelegation: function(event){ //this should really be in event
		if(!event.stopDelegation){
            var killed = false;
			event.stopDelegation = function(){
				killed = true;
			    try{
				    event.stopPropagation(); 
				    event.preventDefault();
			    }catch(e){}
			};
			event.isDelegationStopped = function(){return killed;};
		}	
	},
    /**
     * Used for sorting events on an object
     * @param {Object} a
     * @param {Object} b
     * @return {Number} -1,0,1 depending on how a and b should be sorted.
     */
    sort_by_order: function(a,b){
    	if(a.order < b.order) return 1;
    	if(b.order < a.order) return -1;
    	var ae = a._event, be = b._event;
    	if(ae == 'click' &&  be == 'change') return 1;
    	if(be == 'click' &&  ae == 'change') return -1;
    	return 0;
    },
    /**
     * Stores all delegated events
     */
    events: {},
    onload_called : false
})
jQuery.extend(jQuery.Delegator.prototype,
{
    /*
     * returns the event that should actually be used.  In practice, this is just used to switch focus/blur
     * to activate/deactivate for ie.
     * @return {String} the adjusted event name.
     */
    event: function(){
    	if(jQuery.browser.msie){
            if(this._event == 'focus')
    			return 'activate';
    		else if(this._event == 'blur')
    			return 'deactivate';
    	}
    	return this._event;
    },
    /*
     * Returns if capture should be used (blur and focus)
     * @return {Boolean} true for focus / blur, false if otherwise
     */
    capture: function(){
        return jQuery.Array.include(['focus','blur'],this._event);
    },
    /**
     * If there are no special cases, this is called to add to the delegator.
     * @param {String} selector - css selector
     * @param {String} event - event selector
     * @param {Function} func - a function that will be called
     */
    add_to_delegator: function(selector, event, func){
        var s = selector || this._selector;
        var e = event || this.event();
        var f = func || this._func;
        var delegation_events = jQuery.data(this.element,"delegateEvents");
        if(!delegation_events[e] || delegation_events[e].length == 0){
            var bind_function = jQuery.Function.bind(this.dispatch_event, this)
            jQuery.event.add( this.element , e, bind_function, null, this.capture() );
            delegation_events[e] = [];
            delegation_events[e]._bind_function = bind_function;
		}
		delegation_events[e].push(this);
    },
    _remove_from_delegator : function(event_type){
        var event = event_type || this.event();
        var events = jQuery.data(this.element,"delegateEvents")[event];
        for(var i = 0; i < events.length;i++ ){
            if(events[i] == this){
                events.splice(i, 1);
                break;
            }
        }
        if(events.length == 0){
            jQuery.event.remove(this.element, event, events._bind_function, this.capture() );
        }
    },
    /*
     * Handles the submit case for IE.  It checks if a keypress return happens in an
     * input area or a submit button is clicked.
     */
    submit_for_ie : function(){
		this.add_to_delegator(null, 'click');
        this.add_to_delegator(null, 'keypress');
        
        this.filters= {
			click : function(el, event, parents){
				//check you are in a form
                if(el.nodeName.toUpperCase() == 'INPUT' && el.type.toLowerCase() == 'submit'){
                    for(var e = 0; e< parents.length ; e++) if(parents[e].tag == 'FORM') return true;
                }
                return false;
                
			},
			keypress : function(el, event, parents){
				if(el.nodeName.toUpperCase()!= 'INPUT') return false;
				var res = typeof Prototype != 'undefined' ? (event.keyCode == 13) : (event.charCode == 13)
                if(res){
                    for(var e = 0; e< parents.length ; e++) if(parents[e].tag == 'FORM') return true;
                }
                return false;
			}
		};
	},
    /*
     * Handles change events for IE.
     */
	change_for_ie : function(){
		this.add_to_delegator(null, 'click');
        this.add_to_delegator(null, 'keyup');
        this.add_to_delegator(null, 'beforeactivate')
        //return if right or not
        this.end_filters= {
			click : function(el, event){
				switch(el.nodeName.toLowerCase()){
                    case "select":
                        if(typeof el.selectedIndex == 'undefined') return false;
                        var data = jQuery.data(el, "_change_data", jQuery.data(el, "_change_data") || {} );
                        if( data._change_old_value == null){
        					data._change_old_value = el.selectedIndex.toString();
        					return false;
        				}else{
        					if(data._change_old_value == el.selectedIndex.toString()) return false;
        					data._change_old_value = el.selectedIndex.toString();
        					return true;
        				}
                        break;
                     case "input":
                         if(el.type.toLowerCase() =="checkbox" ) return true;
                         return false;
                     
                }
                return false;
			},
            keyup : function(el, event){
                if(el.nodeName.toLowerCase() != "select") return false;
                if(typeof el.selectedIndex == 'undefined') return false;
                var data = jQuery.data(el, "_change_data", jQuery.data(el, "_change_data") || {} );
                if( data._change_old_value == null){
                    data._change_old_value = el.selectedIndex.toString();
					return false;
				}else{
					if(data._change_old_value == el.selectedIndex.toString()){
                        return false;
                    }
					data._change_old_value = el.selectedIndex.toString();
					return true;
				}
            },
            beforeactivate : function(el, event){
                //we should probably check that onload has been called.
                return el.nodeName.toLowerCase() == 'input' 
                    && el.type.toLowerCase() =="radio" 
                    && !el.checked
                    && jQuery.Delegator.onload_called  //IE selects this on the start
            }
		};
	},
    /*
     * Handles a change event for Safari.
     */
	change_for_webkit : function(){
		this.add_to_delegator(null, 'change');
		this.end_filters= {
			change : function(el, event){
				if(el.nodeName.toLowerCase() == 'input') return true;
                if(typeof el.value == 'undefined') return false; //sometimes it won't exist yet
				var old = el.getAttribute('_old_value');
				el.setAttribute('_old_value', el.value);
				return el.value != old;
			}
		};
	},
    /**
     * Handles a right click for Opera.  It looks for clicks with shiftkey pressed.
     */
    context_for_opera : function(){
        this.add_to_delegator(null, 'click');
        this.end_filters= {
			click : function(el, event){
				return event.shiftKey;
			}
        }
    },
    regexp_patterns:  {tag :    		/^\s*(\*|[\w\-]+)(\b|$)?/,
        				id :            /^#([\w\-\*]+)(\b|$)/,
    					className :     /^\.([\w\-\*]+)(\b|$)/},
    /*
     * returns and caches the select order for the css patern.
     * @retun {Array} array of objects that are used to match with the node_path
     */
    selector_order : function(){
		if(this.order) return this.order;
		var selector_parts = this._selector.split(/\s+/);
		var patterns = this.regexp_patterns;
		var order = [];
        if(this._selector)
		for(var i =0; i< selector_parts.length; i++){
			var v = {}, r, p =selector_parts[i];
			for(var attr in patterns){
				if( patterns.hasOwnProperty(attr) ){
					if( (r = p.match(patterns[attr]))  ) {
						if(attr == 'tag')
							v[attr] = r[1].toUpperCase();
						else
							v[attr] = r[1];
						p = p.replace(r[0],'');
					}
				}
			}
			order.push(v);
		}
		this.order = order;
		return this.order;
	},
    /**
     * Tests if an event matches an element.
     * @param {Object} el the element we are testing
     * @param {Object} event the event
     * @param {Object} parents an array of node order objects for the element
     * @return {Object} returns an object with node, order, and delegation_event attributes.
     */
    match: function(el, event, parents){
        if(this.filters && !this.filters[event.type](el, event, parents)) return null;
		//if(this.controller.className != 'main' &&  (el == document.documentElement || el==document.body) ) return false;
		var matching = 0;
		var selector_order = this.selector_order();
        if(selector_order.length == 0){ //attached to top node
            return {node: parents[0].element, order: 0, delegation_event: this}
        } 
        for(var n=0; n < parents.length; n++){
			var node = parents[n], match = selector_order[matching], matched = true;
			for(var attr in match){
				if(!match.hasOwnProperty(attr) || attr == 'element') continue;
				if(match[attr] && attr == 'className' && node.className){
					if(! jQuery.Array.include(node.className.split(' '),match[attr])) matched = false;
				}else if(match[attr] && node[attr] != match[attr]){
					matched = false;
				}
			}
			if(matched){
				matching++;
                if(matching >= selector_order.length) {
                    if(this.end_filters && !this.end_filters[event.type](el, event)) return null;
                    return {node: node.element, order: n, delegation_event: this};
                }
			}
		}
		return null;
    },
    /**
     * Goes through the delegated events for the given event type (e.g. Click).  Orders the matches
     * by how nested they are in the dom.  Adds the kill function on the event, then dispatches each
     * event.  If kill is called, it will stop dispatching other events.
     * @param {Event} event the DOM event returned by a normal event handler.
     */
	dispatch_event: function(event){
        var target = event.target, matched = false, ret_value = true,matches = [];
		var delegation_events = jQuery.data(this.element,"delegateEvents")[event.type];
        var parents_path = this.node_path(target);
		for(var i =0; i < delegation_events.length;  i++){
			var delegation_event = delegation_events[i];
			var match_result = delegation_event.match(target, event, parents_path);
			if(match_result){
				matches.push(match_result);
			}
		}

		if(matches.length == 0) return true;
		jQuery.Delegator.addStopDelegation(event);
		matches.sort(jQuery.Delegator.sort_by_order);
        var match;
		for(var m = 0; m < matches.length; m++){
            match = matches[m];
            ret_value = match.delegation_event._func.call(match.node,  event)
			if(event.isDelegationStopped()) return false;
		}
	},
    /**
     * Returns an array of objects that represent the path of the node to documentElement.  Each item in the array
     * has a tag, className, id, and element attribute.
     * @param {Object} el element in the dom that is nested under the documentElement
     * @return {Array} representation of the path between the element and the DocumentElement
     */
    node_path: function(el){
        var body = this.element,parents = [],iterator =el;
		if(iterator == body) return [{tag: iterator.nodeName, className: iterator.className, id: iterator.id, element: iterator}]
        do{
            parents.unshift({tag: iterator.nodeName, className: iterator.className, id: iterator.id, element: iterator});
        }while(((iterator = iterator.parentNode) != body )&& iterator)
        if(iterator)
            parents.unshift({tag: iterator.nodeName, className: iterator.className, id: iterator.id, element: iterator});
        return parents;
	},
    destroy : function(){
        //remove from events
        if(this._event == 'contextmenu' && jQuery.browser.opera){
            return this._remove_from_delegator("click");
        }
        if(this._event == 'submit' && jQuery.browser.msie) {
            this._remove_from_delegator("keypress");
            return this._remove_from_delegator("click");
        }
    	if(this._event == 'change' && jQuery.browser.msie){
            this._remove_from_delegator("keyup");
            this._remove_from_delegator("beforeactivate");
            return this._remove_from_delegator("click");
        } 
    	//if(this._event == 'change' && jQuery.browser.safari){
        //    return this._remove_from_delegator();
        //}
        this._remove_from_delegator()
    }
})

Download in other formats:

Original Format