From 6a670df9e9b9e5cd278b390a2e99ac179307a4e9 Mon Sep 17 00:00:00 2001 From: Dave Methvin Date: Sat, 13 Aug 2011 10:46:34 -0400 Subject: [PATCH] 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. --- src/event.js | 261 +++++++++++++++++++++++---------------------- test/unit/event.js | 3 + 2 files changed, 139 insertions(+), 125 deletions(-) diff --git a/src/event.js b/src/event.js index 72cb7bded..511c1b892 100644 --- a/src/event.js +++ b/src/event.js @@ -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; - - var ret = handleObj.handler.apply( target, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); + 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; + + ret = handleObj.handler.apply( target, args ); + + 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 ); - } - }); + setup: function() { + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } - jQuery.event.add(this, "keypress._submit", function( e ) { + jQuery.event.add(this, "click._submit", function( e ) { var elem = e.target, - type = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.type : ""; + type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; - if ( (type === "text" || type === "password") && elem.form && e.keyCode === 13 ) { - trigger( "submit", this, e ); - } - }); - } + 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(); } diff --git a/test/unit/event.js b/test/unit/event.js index 31bc6c92f..9e49a1e13 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -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." ); }); -- 2.39.5