Rework the special events interface to add handle and trigger hooks. Modify IE change/submit special events to take advantage of them. Rewrite mouseover/enter code as special events rather than inline code. In the event unit test, set a tabindex on the div element and focus it first to justify a legitimate blur event.

This commit is contained in:
Dave Methvin 2011-08-13 10:46:34 -04:00 committed by timmywil
parent df6e0d508e
commit 6a670df9e9
2 changed files with 139 additions and 125 deletions

View File

@ -8,17 +8,18 @@ var rnamespaces = /\.(.*)$/,
rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/,
rhoverHack = /\bhover(\.\S+)?/,
rquickIs = /^([\w\-]+)?(?:#([\w\-]+))?(?:\.([\w\-]+))?(?:\[([\w+\-]+)=["']?([\w\-]*)["']?\])?(:first-child|:last-child|:empty)?$/,
delegateTypeMap = {
focus: "focusin",
blur: "focusout",
mouseenter: "mouseover",
mouseleave: "mouseout"
},
quickPseudoMap = {
":empty": "firstChild",
":first-child": "previousSibling",
":last-child": "nextSibling"
};
function useNativeMethod( event ) {
if ( !event.isDefaultPrevented() && this[ event.type ] ) {
this[ event.type ]();
return false;
}
}
/*
* A number of helper functions used for managing events.
@ -75,15 +76,32 @@ jQuery.event = {
tns = rtypenamespace.exec( types[t] ) || [];
type = tns[1];
namespaces = (tns[2] || "").split( "." ).sort();
special = jQuery.event.special[ type ] || {};
type = (selector? special.delegateType : special.bindType ) || type;
handleObj = jQuery.extend({
type: type,
type: type,
origType: tns[1],
data: data,
handler: handler,
guid: handler.guid,
selector: selector,
namespace: namespaces.join(".")
}, handleObjIn);
special = jQuery.event.special[ type ] || {};
// Delegated event setup
if ( selector ) {
// Pre-analyze selector so we can process it quickly on event dispatch
quick = handleObj.quick = rquickIs.exec( selector );
if ( quick ) {
// 0 1 2 3 4 5 6
// [ _, tag, id, class, attrName, attrValue, pseudo(:empty :first-child :last-child) ]
quick[1] = ( quick[1] || "" ).toLowerCase();
quick[3] = quick[3] && new RegExp( "\\b" + quick[3] + "\\b" );
quick[6] = quickPseudoMap[ quick[6] ];
} else if ( jQuery.expr.match.POS.test( selector ) ) {
handleObj.isPositional = true;
}
}
// Init the event handler queue if we're the first
handlers = events[ type ];
@ -103,27 +121,6 @@ jQuery.event = {
}
}
// Delegated event setup
if ( selector ) {
// Rename non-bubbling events to their bubbling counterparts
if ( delegateTypeMap[ type ] ) {
handleObj.preType = type;
handleObj.type = delegateTypeMap[ type ];
}
// Pre-analyze selector so we can process it quickly on event dispatch
quick = handleObj.quick = rquickIs.exec( selector );
if ( quick ) {
// 0 1 2 3 4 5 6
// [ _, tag, id, class, attrName, attrValue, pseudo(:empty :first-child :last-child) ]
quick[1] = ( quick[1] || "" ).toLowerCase();
quick[3] = quick[3] && new RegExp( "\\b" + quick[3] + "\\b" );
quick[6] = quickPseudoMap[ quick[6] ];
} else if ( jQuery.expr.match.POS.test( selector ) ) {
handleObj.isPositional = true;
}
}
if ( special.add ) {
special.add.call( elem, handleObj );
@ -184,6 +181,7 @@ jQuery.event = {
}
special = jQuery.event.special[ type ] || {};
type = (selector? special.delegateType : special.bindType ) || type;
eventType = events[ type ] || [];
namespaces = namespaces? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null;
@ -247,10 +245,15 @@ jQuery.event = {
},
trigger: function( event, data, elem, onlyHandlers ) {
// Don't do events on text and comment nodes
if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
return;
}
// Event object or event type
var type = event.type || event,
namespaces = [],
exclusive, cur, ontype, handle, old, special;
exclusive, origType, cur, ontype, handle, old, special;
if ( type.indexOf("!") >= 0 ) {
// Exclusive events trigger only for the exact event (no namespaces)
@ -306,11 +309,6 @@ jQuery.event = {
return;
}
// Don't do events on text and comment nodes
if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
return;
}
// Clean up the event in case it is being reused
event.result = undefined;
event.target = elem;
@ -319,6 +317,16 @@ jQuery.event = {
data = data != null ? jQuery.makeArray( data ) : [];
data.unshift( event );
// Allow special events to draw outside the lines
special = jQuery.event.special[ type ] || {};
if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
return;
}
origType = type;
if ( special.bindType ) {
type = event.type = special.bindType;
}
// IE doesn't like method names with a colon (#3533, #8272)
ontype = type.indexOf(":") < 0 ? "on" + type : "";
@ -338,13 +346,18 @@ jQuery.event = {
event.preventDefault();
}
// May need to change the event type when bubbling special events
if ( cur === elem && special.delegateType ) {
event.type = special.delegateType;
}
// Bubble up to document, then to window; watch for a global parentNode or ownerDocument var (#9724)
cur = (!cur.setInterval && cur.parentNode || cur.ownerDocument) || cur === event.target.ownerDocument && window;
} while ( cur && !event.isPropagationStopped() );
type = event.type = origType;
// If nobody prevented the default action, do it now
if ( !event.isDefaultPrevented() ) {
special = jQuery.event.special[ type ] || {};
if ( (!special._default || special._default.call( elem.ownerDocument, event, data ) === false) &&
!(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
@ -386,7 +399,7 @@ jQuery.event = {
delegateCount = handlers.delegateCount,
args = Array.prototype.slice.call( arguments, 0 ),
handlerQueue = [],
i, cur, selMatch, matches, handleObj, sel, matched, related;
i, cur, selMatch, matches, handleObj, sel, hit, related;
// Use the fix-ed jQuery.Event rather than the (read-only) native event
args[0] = event;
@ -401,25 +414,16 @@ jQuery.event = {
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
sel = handleObj.selector;
hit = selMatch[ sel ];
if ( handleObj.isPositional ) {
// Since .is() does not work for positionals; see http://jsfiddle.net/eJ4yd/3/
matched = (selMatch[ sel ] || (selMatch[ sel ] = jQuery( sel ))).index( cur ) >= 0;
hit = (hit || (selMatch[ sel ] = jQuery( sel ))).index( cur ) >= 0;
} else {
matched = selMatch[ sel ] || selMatch[ sel ] !== false && (selMatch[ sel ] = (handleObj.quick? quickIs( cur, handleObj.quick ) : jQuery( cur ).is( sel )));
hit = hit || hit !== false && (selMatch[ sel ] = (handleObj.quick? quickIs( cur, handleObj.quick ) : jQuery( cur ).is( sel )));
}
if ( matched ) {
related = null;
if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) {
// Don't match a child element with the same selector
related = jQuery( event.relatedTarget ).closest( sel )[0];
if ( related && jQuery.contains( cur, related ) ) {
related = cur;
}
}
if ( !related || related !== cur ) {
matches.push( handleObj );
}
if ( hit ) {
matches.push( handleObj );
}
}
if ( matches.length ) {
@ -434,7 +438,8 @@ jQuery.event = {
// Run delegates first; they may want to stop propagation beneath us
event.delegateTarget = this;
for ( i=0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
dispatch( handlerQueue[i].elem, event, handlerQueue[i].matches, args );
matched = handlerQueue[i];
dispatch( matched.elem, event, matched.matches, args );
}
delete event.delegateTarget;
@ -519,6 +524,15 @@ jQuery.event = {
// Make sure the ready event is setup
setup: jQuery.bindReady
},
focus: {
delegateType: "focusin",
trigger: useNativeMethod
},
blur: {
delegateType: "focusout",
trigger: useNativeMethod
},
beforeunload: {
setup: function( data, namespaces, eventHandle ) {
@ -539,29 +553,33 @@ jQuery.event = {
// Run jQuery handler functions; called from jQuery.event.handle
function dispatch( target, event, handlers, args ) {
var run_all = !event.exclusive && !event.namespace;
var run_all = !event.exclusive && !event.namespace,
specialHandle = (jQuery.event.special[event.type] || {}).handle,
j, handleObj, ret;
event.currentTarget = target;
for ( var j = 0, l = handlers.length; j < l && !event.isImmediatePropagationStopped(); j++ ) {
var handleObj = handlers[ j ];
for ( j = 0, l = handlers.length; j < l && !event.isImmediatePropagationStopped(); j++ ) {
handleObj = handlers[ j ];
// Triggered event must either 1) be non-exclusive and have no namespace, or
// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
// Pass in a reference to the handler function itself
// So that we can later remove it
event.handler = handleObj.handler;
event.data = handleObj.data;
event.handleObj = handleObj;
if ( !specialHandle || specialHandle.call( target, event, handleObj ) === false ) {
// Pass in a reference to the handler function itself
// So that we can later remove it
event.handler = handleObj.handler;
event.data = handleObj.data;
event.handleObj = handleObj;
var ret = handleObj.handler.apply( target, args );
ret = handleObj.handler.apply( target, args );
if ( ret !== undefined ) {
event.result = ret;
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
if ( ret !== undefined ) {
event.result = ret;
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
@ -578,6 +596,7 @@ function quickIs( elem, m ) {
);
}
jQuery.removeEvent = document.removeEventListener ?
function( elem, type, handle ) {
if ( elem.removeEventListener ) {
@ -673,44 +692,30 @@ jQuery.Event.prototype = {
isImmediatePropagationStopped: returnFalse
};
// Checks if an event happened on an element within another element
// Used in jQuery.event.special.mouseenter and mouseleave handlers
var withinElement = function( event ) {
// Check if mouse(over|out) are still within the same parent element
var related = event.relatedTarget,
inside = false,
eventType = event.type;
event.type = event.data;
if ( related !== this ) {
if ( related ) {
inside = jQuery.contains( this, related );
}
if ( !inside ) {
jQuery.event.handle.apply( this, arguments );
event.type = eventType;
}
}
};
// Create mouseenter and mouseleave events
// Create mouseenter and mouseleave events; IE has its own native ones but
// we need to support event delegation as well so we don't use them.
jQuery.each({
mouseenter: "mouseover",
mouseleave: "mouseout"
}, function( orig, fix ) {
jQuery.event.special[ orig ] = {
//TODO: fix this break and add a useful test case
setup: function( data ) {
jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
},
teardown: function( data ) {
jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
jQuery.event.special[ orig ] = jQuery.event.special[ fix ] = {
delegateType: fix,
bindType: fix,
handle: function( event, handleObj ) {
var target = this,
related = event.relatedTarget,
selector = handleObj.selector;
if ( selector && related ) {
// Delegated event; find the real relatedTarget
related = jQuery( related ).closest( selector )[0];
}
if ( !related || related !== target && !jQuery.contains( target, related ) ) {
//TODO: don't clobber event.type permanently
event.type = handleObj.origType;
return false;
}
}
};
});
@ -719,30 +724,32 @@ jQuery.each({
if ( !jQuery.support.submitBubbles ) {
jQuery.event.special.submit = {
setup: function( data, namespaces ) {
if ( !jQuery.nodeName( this, "form" ) ) {
jQuery.event.add(this, "click._submit", function( e ) {
var elem = e.target,
type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : "";
trigger( "submit", this, arguments );
if ( (type === "submit" || type === "image") && elem.form ) {
trigger( "submit", this, e );
}
});
jQuery.event.add(this, "keypress._submit", function( e ) {
var elem = e.target,
type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : "";
if ( (type === "text" || type === "password") && elem.form && e.keyCode === 13 ) {
trigger( "submit", this, e );
}
});
setup: function() {
if ( jQuery.nodeName( this, "form" ) ) {
return false;
}
jQuery.event.add(this, "click._submit", function( e ) {
var elem = e.target,
type = jQuery.nodeName( elem, "input" ) ? elem.type : "";
trigger( "submit", this, arguments );
if ( (type === "submit" || type === "image") && elem.form ) {
simulate( "submit", this, e );
}
});
jQuery.event.add(this, "keypress._submit", function( e ) {
var elem = e.target,
type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : "";
if ( (type === "text" || type === "password") && elem.form && e.keyCode === 13 ) {
simulate( "submit", this, e );
}
});
},
teardown: function( namespaces ) {
teardown: function() {
jQuery.event.remove( this, "._submit" );
}
};
@ -793,8 +800,8 @@ if ( !jQuery.support.changeBubbles ) {
jQuery._data( elem, "_change_data", val );
}
if ( val !== old && old != null || val ) {
trigger( "change", elem, e );
if ( val !== old && old != null ) {
simulate( "change", elem, e, true );
}
},
@ -865,12 +872,16 @@ if ( !jQuery.support.changeBubbles ) {
};
}
function trigger( type, elem, event ) {
function simulate( type, elem, event, bubble ) {
// Piggyback on a donor event to simulate a different one.
// Fake originalEvent to avoid donor's stopPropagation, but if the
// simulated event prevents default then we do the same on the donor.
var e = jQuery.extend( {}, event, { type: type, originalEvent: {} } );
jQuery.event.handle.call( elem, e );
if ( bubble ) {
jQuery.event.trigger( e, null, elem );
} else {
jQuery.event.handle.call( elem, e );
}
if ( e.isDefaultPrevented() ) {
event.preventDefault();
}

View File

@ -1457,6 +1457,9 @@ test(".live()/.die()", function() {
jQuery("#nothiddendiv div").die("click");
// div must have a tabindex to be focusable
jQuery("#nothiddendiv div").attr("tabindex", "0")[0].focus();
jQuery("#nothiddendiv div").live("blur", function(){
ok( true, "Live div trigger blur." );
});