Permalink
Browse files

Event: Remove fixHooks, propHooks; switch to ES5 getter with addProp

Fixes gh-3103
Fixes gh-1746
Closes gh-2860

- Removes the copy loop in jQuery.event.fix
- Avoids accessing properties such as client/offset/page/screen X/Y
  which may cause style recalc or layouts
- Simplifies adding property hooks to event object
  • Loading branch information...
jbedard authored and dmethvin committed Nov 1, 2013
1 parent 7f2ebd2 commit e61fccb9d736235b4b011f89cba6866bc0b8997d
Showing with 122 additions and 96 deletions.
  1. +115 −81 src/event.js
  2. +7 −15 test/unit/event.js
View
@@ -398,90 +398,38 @@ jQuery.event = {
return handlerQueue;
},
// Includes some event props shared by KeyEvent and MouseEvent
props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " +
"metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ),
fixHooks: {},
keyHooks: {
props: "char charCode key keyCode".split( " " ),
filter: function( event, original ) {
// Add which for key events
if ( event.which == null ) {
event.which = original.charCode != null ? original.charCode : original.keyCode;
addProp: function( name, hook ) {
Object.defineProperty( jQuery.Event.prototype, name, {
enumerable: true,
configurable: true,
get: jQuery.isFunction( hook ) ?
function() {
if ( this.originalEvent ) {
return hook( this.originalEvent );
}
} :
function() {
if ( this.originalEvent ) {
return this.originalEvent[ name ];
}
},
set: function( value ) {
Object.defineProperty( this, name, {
enumerable: true,
configurable: true,
writable: true,
value: value
} );
}
return event;
}
} );
},
mouseHooks: {
props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " +
"screenX screenY toElement" ).split( " " ),
filter: function( event, original ) {
var eventDoc, doc, body,
button = original.button;
// Calculate pageX/Y if missing and clientX/Y available
if ( event.pageX == null && original.clientX != null ) {
eventDoc = event.target.ownerDocument || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = original.clientX +
( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
( doc && doc.clientLeft || body && body.clientLeft || 0 );
event.pageY = original.clientY +
( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
( doc && doc.clientTop || body && body.clientTop || 0 );
}
// Add which for click: 1 === left; 2 === middle; 3 === right
// Note: button is not normalized, so don't use it
if ( !event.which && button !== undefined ) {
event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
}
return event;
}
},
fix: function( event ) {
if ( event[ jQuery.expando ] ) {
return event;
}
// Create a writable copy of the event object and normalize some properties
var i, prop, copy,
type = event.type,
originalEvent = event,
fixHook = this.fixHooks[ type ];
if ( !fixHook ) {
this.fixHooks[ type ] = fixHook =
rmouseEvent.test( type ) ? this.mouseHooks :
rkeyEvent.test( type ) ? this.keyHooks :
{};
}
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
event = new jQuery.Event( originalEvent );
i = copy.length;
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}
// Support: Safari <=6 - 7 only
// Target should not be a text node (#504, #13143)
if ( event.target.nodeType === 3 ) {
event.target = event.target.parentNode;
}
return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
fix: function( originalEvent ) {
return originalEvent[ jQuery.expando ] ?
originalEvent :
new jQuery.Event( originalEvent );
},
special: {
@@ -569,6 +517,16 @@ jQuery.Event = function( src, props ) {
returnTrue :
returnFalse;
// Create target properties
// Support: Safari <=6 - 7 only
// Target should not be a text node (#504, #13143)
this.target = ( src.target.nodeType === 3 ) ?
src.target.parentNode :
src.target;
this.currentTarget = src.currentTarget;
this.relatedTarget = src.relatedTarget;
// Event type
} else {
this.type = src;
@@ -625,6 +583,82 @@ jQuery.Event.prototype = {
}
};
// Includes all common event props including KeyEvent and MouseEvent specific props
jQuery.each( {
altKey: true,
bubbles: true,
cancelable: true,
ctrlKey: true,
detail: true,
eventPhase: true,
metaKey: true,
shiftKey: true,
view: true,
"char": true,
charCode: true,
key: true,
keyCode: true,
button: true,
buttons: true,
clientX: true,
clientY: true,
offsetX: true,
offsetY: true,
screenX: true,
screenY: true,
toElement: true,
which: function( event ) {
var button = event.button;
// Add which for key events
if ( event.which == null && rkeyEvent.test( event.type ) ) {
return event.charCode != null ? event.charCode : event.keyCode;
}
// Add which for click: 1 === left; 2 === middle; 3 === right
if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
return ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
}
return event.which;
},
pageX: function( event ) {
var eventDoc, doc, body;
// Calculate pageX if missing and clientX available
if ( event.pageX == null && event.clientX != null ) {
eventDoc = event.target.ownerDocument || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
return event.clientX +
( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
( doc && doc.clientLeft || body && body.clientLeft || 0 );
}
return event.pageX;
},
pageY: function( event ) {
var eventDoc, doc, body;
// Calculate pageY if missing and clientY available
if ( event.pageY == null && event.clientY != null ) {
eventDoc = event.target.ownerDocument || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
return event.clientY +
( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
( doc && doc.clientTop || body && body.clientTop || 0 );
}
return event.pageY;
}
}, jQuery.event.addProp );
// Create mouseenter/leave events using mouseover/out and event-time checks
// so that event delegation works in jQuery.
// Do the same for pointerenter/pointerleave and pointerover/pointerout
View
@@ -2410,36 +2410,28 @@ QUnit.test( "event object properties on natively-triggered event", function( ass
$link.off( "click" ).remove();
} );
QUnit.test( "fixHooks extensions", function( assert ) {
QUnit.test( "addProp extensions", function( assert ) {
assert.expect( 2 );
// IE requires focusable elements to be visible, so append to body
var $fixture = jQuery( "<input type='text' id='hook-fixture' />" ).appendTo( "body" ),
saved = jQuery.event.fixHooks.click;
var $fixture = jQuery( "<div>" ).appendTo( "#qunit-fixture" );
// Ensure the property doesn't exist
$fixture.on( "click", function( event ) {
assert.ok( !( "blurrinessLevel" in event ), "event.blurrinessLevel does not exist" );
assert.ok( !( "testProperty" in event ), "event.testProperty does not exist" );
} );
fireNative( $fixture[ 0 ], "click" );
$fixture.off( "click" );
jQuery.event.fixHooks.click = {
filter: function( event ) {
event.blurrinessLevel = 42;
return event;
}
};
jQuery.event.addProp( "testProperty", function() { return 42; } );
// Trigger a native click and ensure the property is set
$fixture.on( "click", function( event ) {
assert.equal( event.blurrinessLevel, 42, "event.blurrinessLevel was set" );
assert.equal( event.testProperty, 42, "event.testProperty getter was invoked" );
} );
fireNative( $fixture[ 0 ], "click" );
$fixture.off( "click" );
delete jQuery.event.fixHooks.click;
$fixture.off( "click" ).remove();
jQuery.event.fixHooks.click = saved;
$fixture.remove();
} );
QUnit.test( "drag/drop events copy mouse-related event properties (gh-1925, gh-2009)", function( assert ) {

3 comments on commit e61fccb

@mgol

This comment has been minimized.

Show comment
Hide comment
@mgol

mgol May 6, 2016

Member

@jbedard This commit is not counted as yours as the email is not associated. :) The earlier ones are taken into account so you might want to add this email to your account (AFAIK it doesn't have to be displayed anywhere).

Member

mgol replied May 6, 2016

@jbedard This commit is not counted as yours as the email is not associated. :) The earlier ones are taken into account so you might want to add this email to your account (AFAIK it doesn't have to be displayed anywhere).

@dmethvin

This comment has been minimized.

Show comment
Hide comment
@dmethvin

dmethvin May 6, 2016

Member

Oh good catch! Once @jbedard accepts the org invite I'll assign the completed ticket to him as well.

Member

dmethvin replied May 6, 2016

Oh good catch! Once @jbedard accepts the org invite I'll assign the completed ticket to him as well.

@jbedard

This comment has been minimized.

Show comment
Hide comment
@jbedard

jbedard May 6, 2016

Contributor

Thanks for pointing that out!

Contributor

jbedard replied May 6, 2016

Thanks for pointing that out!

Please sign in to comment.