The library for web and native user interfaces.
Delegate Event Classes
React's top-level event delegation dispatches `AbstractEvent` objects that contain: - `nativeEvent`, the original browser event. - `data`, an object with custom normalized properties. This diff creates a set of `DelegateEvent` classes that will replace `AbstractEvent`. The goal is two-fold: # Provide a cross-browser implementation that conforms to the DOM Level 3 Events API so people don't have to use `nativeEvent`. # Generalize the event object API so that it can be shared by `DOMEventManager`, a top-level event delegation WIP. This simply implements the classes. I will follow-up by replacing `AbstractEvent` with them.
CommitSyncScript committed
Jun 7, 2013 at 22:04 UTC
3eaed5a122a050a6a2852b9f5348685373a9a6f7
11 files changed
+559
src/event/delegate/DelegateEvent.js
new
+140
@@ -0,0 +1,140 @@
1
+/**
2
+ * @providesModule DelegateEvent
3
+ * @typechecks
4
+ */
5
+
6
+var PooledClass = require('PooledClass');
7
+
8
+var emptyFunction = require('emptyFunction');
9
+var getEventTarget = require('getEventTarget');
10
+var merge = require('merge');
11
+var mergeInto = require('mergeInto');
12
+
13
+/**
14
+ * @interface Event
15
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
16
+ */
17
+var EventInterface = {
18
+ type: null,
19
+ target: getEventTarget,
20
+ currentTarget: null,
21
+ eventPhase: null,
22
+ bubbles: null,
23
+ cancelable: null,
24
+ timeStamp: function(event) {
25
+ return event.timeStamp || Date.now();
26
+ },
27
+ defaultPrevented: null,
28
+ isTrusted: null
29
+};
30
+
31
+/**
32
+ * Delegate events are dispatched by top-level event delegation systems.
33
+ *
34
+ * These systems should generally use pooling to reduce the frequency of garbage
35
+ * collection. The system should check `isPersistent` to determine whether the
36
+ * event should be released into the pool after being dispatched. Users that
37
+ * need a persisted event should invoke `persist`.
38
+ *
39
+ * Delegate events (and subclasses) implement the DOM Level 3 Events API by
40
+ * normalizing browser quirks. Subclasses do not necessarily have to implement a
41
+ * DOM interface; custom application-specific events can also subclass this.
42
+ *
43
+ * @param {object} delegateConfig Configuration used by top-level delegation.
44
+ * @param {string} delegateMarker Marker identifying the event target.
45
+ * @param {object} nativeEvent Native browser event.
46
+ */
47
+function DelegateEvent(delegateConfig, delegateMarker, nativeEvent) {
48
+ this.delegateConfig = delegateConfig;
49
+ this.delegateMarker = delegateMarker;
50
+ this.nativeEvent = nativeEvent;
51
+
52
+ var Interface = this.constructor.Interface;
53
+ for (var propName in Interface) {
54
+ var normalize = Interface[propName];
55
+ if (normalize) {
56
+ this[propName] = normalize(nativeEvent);
57
+ } else {
58
+ this[propName] = nativeEvent[propName];
59
+ }
60
+ }
61
+
62
+ if (nativeEvent.defaultPrevented || nativeEvent.returnValue === false) {
63
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
64
+ } else {
65
+ this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
66
+ }
67
+ this.isPropagationStopped = emptyFunction.thatReturnsFalse;
68
+}
69
+
70
+mergeInto(DelegateEvent.prototype, {
71
+
72
+ preventDefault: function() {
73
+ this.defaultPrevented = true;
74
+ var event = this.nativeEvent;
75
+ event.preventDefault ? event.preventDefault() : event.returnValue = false;
76
+ this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
77
+ },
78
+
79
+ stopPropagation: function() {
80
+ var event = this.nativeEvent;
81
+ event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
82
+ this.isPropagationStopped = emptyFunction.thatReturnsTrue;
83
+ },
84
+
85
+ /**
86
+ * We release all dispatched `DelegateEvent`s after each event loop, adding
87
+ * them back into the pool. This allows a way to hold onto a reference that
88
+ * won't be added back into the pool.
89
+ */
90
+ persist: function() {
91
+ this.isPersistent = emptyFunction.thatReturnsTrue;
92
+ },
93
+
94
+ /**
95
+ * Checks if this event should be released back into the pool.
96
+ *
97
+ * @return {boolean} True if this should not be released, false otherwise.
98
+ */
99
+ isPersistent: emptyFunction.thatReturnsFalse,
100
+
101
+ /**
102
+ * `PooledClass` looks for `destructor` on each instance it releases.
103
+ */
104
+ destructor: function() {
105
+ var Interface = this.constructor.Interface;
106
+ for (var propName in Interface) {
107
+ this[propName] = null;
108
+ }
109
+ this.delegateConfig = null;
110
+ this.delegateMarker = null;
111
+ this.nativeEvent = null;
112
+ }
113
+
114
+});
115
+
116
+DelegateEvent.Interface = EventInterface;
117
+
118
+/**
119
+ * Helper to reduce boilerplate when creating subclasses.
120
+ *
121
+ * @param {function} Class
122
+ * @param {?object} Interface
123
+ */
124
+DelegateEvent.augmentClass = function(Class, Interface) {
125
+ var Super = this;
126
+
127
+ var prototype = Object.create(Super.prototype);
128
+ mergeInto(prototype, Class.prototype);
129
+ Class.prototype = prototype;
130
+ Class.prototype.constructor = Class;
131
+
132
+ Class.Interface = merge(Super.Interface, Interface);
133
+ Class.augmentClass = Super.augmentClass;
134
+
135
+ PooledClass.addPoolingTo(Class, PooledClass.threeArgumentPooler);
136
+};
137
+
138
+PooledClass.addPoolingTo(DelegateEvent, PooledClass.threeArgumentPooler);
139
+
140
+module.exports = DelegateEvent;
src/event/delegate/DelegateFocusEvent.js
new
+28
@@ -0,0 +1,28 @@
1
+/**
2
+ * @providesModule DelegateFocusEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateUIEvent = require('DelegateUIEvent');
7
+
8
+/**
9
+ * @interface FocusEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var FocusEventInterface = {
13
+ relatedTarget: null
14
+};
15
+
16
+/**
17
+ * @param {object} reactEventType See `EventPluginHub`.
18
+ * @param {string} reactTargetID ID of the target component.
19
+ * @param {object} nativeEvent Native browser event.
20
+ * @extends {DelegateUIEvent}
21
+ */
22
+function DelegateFocusEvent(reactEventType, reactTargetID, nativeEvent) {
23
+ DelegateUIEvent.call(this, reactEventType, reactTargetID, nativeEvent);
24
+}
25
+
26
+DelegateUIEvent.augmentClass(DelegateFocusEvent, FocusEventInterface);
27
+
28
+module.exports = DelegateFocusEvent;
src/event/delegate/DelegateKeyboardEvent.js
new
+40
@@ -0,0 +1,40 @@
1
+/**
2
+ * @providesModule DelegateKeyboardEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateUIEvent = require('DelegateUIEvent');
7
+
8
+/**
9
+ * @interface KeyboardEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var KeyboardEventInterface = {
13
+ char: null,
14
+ key: null,
15
+ location: null,
16
+ ctrlKey: null,
17
+ shiftKey: null,
18
+ altKey: null,
19
+ metaKey: null,
20
+ repeat: null,
21
+ locale: null,
22
+ // Legacy Interface
23
+ charCode: null,
24
+ keyCode: null,
25
+ which: null
26
+};
27
+
28
+/**
29
+ * @param {object} reactEventType See `EventPluginHub`.
30
+ * @param {string} reactTargetID ID of the target component.
31
+ * @param {object} nativeEvent Native browser event.
32
+ * @extends {DelegateUIEvent}
33
+ */
34
+function DelegateKeyboardEvent(reactEventType, reactTargetID, nativeEvent) {
35
+ DelegateUIEvent.call(this, reactEventType, reactTargetID, nativeEvent);
36
+}
37
+
38
+DelegateUIEvent.augmentClass(DelegateKeyboardEvent, KeyboardEventInterface);
39
+
40
+module.exports = DelegateKeyboardEvent;
src/event/delegate/DelegateMouseEvent.js
new
+69
@@ -0,0 +1,69 @@
1
+/**
2
+ * @providesModule DelegateMouseEvent
3
+ * @typechecks
4
+ */
5
+
6
+var BrowserEnv = require('BrowserEnv');
7
+var DelegateUIEvent = require('DelegateUIEvent');
8
+
9
+/**
10
+ * @interface MouseEvent
11
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
12
+ */
13
+var MouseEventInterface = {
14
+ screenX: null,
15
+ screenY: null,
16
+ clientX: null,
17
+ clientY: null,
18
+ ctrlKey: null,
19
+ shiftKey: null,
20
+ altKey: null,
21
+ metaKey: null,
22
+ button: function(event) {
23
+ // Webkit, Firefox, IE9+
24
+ // which: 1 2 3
25
+ // button: 0 1 2 (standard)
26
+ var button = event.button;
27
+ if ('which' in event) {
28
+ return button;
29
+ }
30
+ // IE<9
31
+ // which: undefined
32
+ // button: 0 0 0
33
+ // button: 1 4 2 (onmouseup)
34
+ return button === 2 ? 2 : button === 4 ? 1 : 0;
35
+ },
36
+ buttons: null,
37
+ relatedTarget: function(event) {
38
+ return event.relatedTarget || (
39
+ event.fromElement === event.srcElement ?
40
+ event.toElement :
41
+ event.fromElement
42
+ );
43
+ },
44
+ // "Proprietary" Interface.
45
+ pageX: function(event) {
46
+ return 'pageX' in event ?
47
+ event.pageX :
48
+ event.clientX + BrowserEnv.currentPageScrollLeft;
49
+ },
50
+ pageY: function(event) {
51
+ return 'pageY' in event ?
52
+ event.pageY :
53
+ event.clientY + BrowserEnv.currentPageScrollTop;
54
+ }
55
+};
56
+
57
+/**
58
+ * @param {object} reactEventType See `EventPluginHub`.
59
+ * @param {string} reactTargetID ID of the target component.
60
+ * @param {object} nativeEvent Native browser event.
61
+ * @extends {DelegateUIEvent}
62
+ */
63
+function DelegateMouseEvent(reactEventType, reactTargetID, nativeEvent) {
64
+ DelegateUIEvent.call(this, reactEventType, reactTargetID, nativeEvent);
65
+}
66
+
67
+DelegateUIEvent.augmentClass(DelegateMouseEvent, MouseEventInterface);
68
+
69
+module.exports = DelegateMouseEvent;
src/event/delegate/DelegateMutationEvent.js
new
+32
@@ -0,0 +1,32 @@
1
+/**
2
+ * @providesModule DelegateMutationEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateEvent = require('DelegateEvent');
7
+
8
+/**
9
+ * @interface MutationEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var MutationEventInterface = {
13
+ relatedNode: null,
14
+ prevValue: null,
15
+ newValue: null,
16
+ attrName: null,
17
+ attrChange: null
18
+};
19
+
20
+/**
21
+ * @param {object} reactEventType See `EventPluginHub`.
22
+ * @param {string} reactTargetID ID of the target component.
23
+ * @param {object} nativeEvent Native browser event.
24
+ * @extends {DelegateEvent}
25
+ */
26
+function DelegateMutationEvent(reactEventType, reactTargetID, nativeEvent) {
27
+ DelegateEvent.call(this, reactEventType, reactTargetID, nativeEvent);
28
+}
29
+
30
+DelegateEvent.augmentClass(DelegateMutationEvent, MutationEventInterface);
31
+
32
+module.exports = DelegateMutationEvent;
src/event/delegate/DelegateTouchEvent.js
new
+34
@@ -0,0 +1,34 @@
1
+/**
2
+ * @providesModule DelegateTouchEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateUIEvent = require('DelegateUIEvent');
7
+
8
+/**
9
+ * @interface TouchEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var TouchEventInterface = {
13
+ touches: null,
14
+ targetTouches: null,
15
+ changedTouches: null,
16
+ altKey: null,
17
+ metaKey: null,
18
+ ctrlKey: null,
19
+ shiftKey: null
20
+};
21
+
22
+/**
23
+ * @param {object} reactEventType See `EventPluginHub`.
24
+ * @param {string} reactTargetID ID of the target component.
25
+ * @param {object} nativeEvent Native browser event.
26
+ * @extends {DelegateUIEvent}
27
+ */
28
+function DelegateTouchEvent(reactEventType, reactTargetID, nativeEvent) {
29
+ DelegateUIEvent.call(this, reactEventType, reactTargetID, nativeEvent);
30
+}
31
+
32
+DelegateUIEvent.augmentClass(DelegateTouchEvent, TouchEventInterface);
33
+
34
+module.exports = DelegateTouchEvent;
src/event/delegate/DelegateUIEvent.js
new
+29
@@ -0,0 +1,29 @@
1
+/**
2
+ * @providesModule DelegateUIEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateEvent = require('DelegateEvent');
7
+
8
+/**
9
+ * @interface UIEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var UIEventInterface = {
13
+ view: null,
14
+ detail: null
15
+};
16
+
17
+/**
18
+ * @param {object} delegateConfig Configuration used by top-level delegation.
19
+ * @param {string} delegateMarker Marker identifying the event target.
20
+ * @param {object} nativeEvent Native browser event.
21
+ * @extends {DelegateEvent}
22
+ */
23
+function DelegateUIEvent(delegateConfig, delegateMarker, nativeEvent) {
24
+ DelegateEvent.call(this, delegateConfig, delegateMarker, nativeEvent);
25
+}
26
+
27
+DelegateEvent.augmentClass(DelegateUIEvent, UIEventInterface);
28
+
29
+module.exports = DelegateUIEvent;
src/event/delegate/DelegateWheelEvent.js
new
+47
@@ -0,0 +1,47 @@
1
+/**
2
+ * @providesModule DelegateWheelEvent
3
+ * @typechecks
4
+ */
5
+
6
+var DelegateMouseEvent = require('DelegateMouseEvent');
7
+
8
+/**
9
+ * @interface WheelEvent
10
+ * @see http://www.w3.org/TR/DOM-Level-3-Events/
11
+ */
12
+var WheelEventInterface = {
13
+ deltaX: function(event) {
14
+ // NOTE: IE<9 does not support x-axis delta.
15
+ return (
16
+ 'deltaX' in event ? event.deltaX :
17
+ // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
18
+ 'wheelDeltaX' in event ? -event.wheelDeltaX : 0
19
+ );
20
+ },
21
+ deltaY: function(event) {
22
+ return (
23
+ // Normalize (up is positive).
24
+ 'deltaY' in event ? -event.deltaY :
25
+ // Fallback to `wheelDeltaY` for Webkit.
26
+ 'wheelDeltaY' in event ? event.wheelDeltaY :
27
+ // Fallback to `wheelDelta` for IE<9.
28
+ 'wheelDelta' in event ? event.wheelData : 0
29
+ );
30
+ },
31
+ deltaZ: null,
32
+ deltaMode: null
33
+};
34
+
35
+/**
36
+ * @param {object} delegateConfig Configuration used by top-level delegation.
37
+ * @param {string} delegateMarker Marker identifying the event target.
38
+ * @param {object} nativeEvent Native browser event.
39
+ * @extends {DelegateMouseEvent}
40
+ */
41
+function DelegateWheelEvent(delegateConfig, delegateMarker, nativeEvent) {
42
+ DelegateMouseEvent.call(this, delegateConfig, delegateMarker, nativeEvent);
43
+}
44
+
45
+DelegateMouseEvent.augmentClass(DelegateWheelEvent, WheelEventInterface);
46
+
47
+module.exports = DelegateWheelEvent;
src/event/delegate/__tests__/DelegateEvent-test.js
new
+65
@@ -0,0 +1,65 @@
1
+/**
2
+ * @emails react-core
3
+ */
4
+
5
+"use strict";
6
+
7
+var DelegateEvent;
8
+
9
+describe('DelegateEvent', function() {
10
+ var delegate;
11
+
12
+ beforeEach(function() {
13
+ DelegateEvent = require('DelegateEvent');
14
+
15
+ delegate = function(nativeEvent) {
16
+ return DelegateEvent.getPooled({}, '', nativeEvent);
17
+ };
18
+ });
19
+
20
+ it('should normalize `target` from the nativeEvent', function() {
21
+ var target = document.createElement('div');
22
+ var delegateEvent = delegate({srcElement: target});
23
+
24
+ expect(delegateEvent.target).toBe(target);
25
+ expect(delegateEvent.type).toBe(undefined);
26
+ });
27
+
28
+ it('should be able to `preventDefault`', function() {
29
+ var nativeEvent = {};
30
+ var delegateEvent = delegate(nativeEvent);
31
+
32
+ expect(delegateEvent.isDefaultPrevented()).toBe(false);
33
+ delegateEvent.preventDefault();
34
+ expect(delegateEvent.isDefaultPrevented()).toBe(true);
35
+
36
+ expect(delegateEvent.defaultPrevented).toBe(true);
37
+
38
+ expect(nativeEvent.returnValue).toBe(false);
39
+ });
40
+
41
+ it('should be prevented if nativeEvent is prevented', function() {
42
+ expect(delegate({defaultPrevented: true}).isDefaultPrevented()).toBe(true);
43
+ expect(delegate({returnValue: false}).isDefaultPrevented()).toBe(true);
44
+ });
45
+
46
+ it('should be able to `stopPropagation`', function() {
47
+ var nativeEvent = {};
48
+ var delegateEvent = delegate(nativeEvent);
49
+
50
+ expect(delegateEvent.isPropagationStopped()).toBe(false);
51
+ delegateEvent.stopPropagation();
52
+ expect(delegateEvent.isPropagationStopped()).toBe(true);
53
+
54
+ expect(nativeEvent.cancelBubble).toBe(true);
55
+ });
56
+
57
+ it('should be able to `persist`', function() {
58
+ var delegateEvent = delegate({});
59
+
60
+ expect(delegateEvent.isPersistent()).toBe(false);
61
+ delegateEvent.persist();
62
+ expect(delegateEvent.isPersistent()).toBe(true);
63
+ });
64
+
65
+});
src/event/delegate/__tests__/DelegateWheelEvent-test.js
new
+63
@@ -0,0 +1,63 @@
1
+/**
2
+ * @emails react-core
3
+ */
4
+
5
+"use strict";
6
+
7
+var DelegateWheelEvent;
8
+
9
+describe('DelegateWheelEvent', function() {
10
+ var delegate;
11
+
12
+ beforeEach(function() {
13
+ DelegateWheelEvent = require('DelegateWheelEvent');
14
+
15
+ delegate = function(nativeEvent) {
16
+ return DelegateWheelEvent.getPooled({}, '', nativeEvent);
17
+ };
18
+ });
19
+
20
+ it('should normalize properties from the Event interface', function() {
21
+ var target = document.createElement('div');
22
+ var delegateEvent = delegate({srcElement: target});
23
+
24
+ expect(delegateEvent.target).toBe(target);
25
+ expect(delegateEvent.type).toBe(undefined);
26
+ });
27
+
28
+ it('should normalize properties from the MouseEvent interface', function() {
29
+ expect(delegate({which: 2, button: 1}).button).toBe(1);
30
+ });
31
+
32
+ it('should normalize properties from the WheelEvent interface', function() {
33
+ var standardEvent = delegate({deltaX: 10, deltaY: -50});
34
+ expect(standardEvent.deltaX).toBe(10);
35
+ expect(standardEvent.deltaY).toBe(50);
36
+
37
+ var webkitEvent = delegate({wheelDeltaX: -10, wheelDeltaY: 50});
38
+ expect(webkitEvent.deltaX).toBe(10);
39
+ expect(webkitEvent.deltaY).toBe(50);
40
+ });
41
+
42
+ it('should be able to `preventDefault` and `stopPropagation`', function() {
43
+ var nativeEvent = {};
44
+ var delegateEvent = delegate(nativeEvent);
45
+
46
+ expect(delegateEvent.isDefaultPrevented()).toBe(false);
47
+ delegateEvent.preventDefault();
48
+ expect(delegateEvent.isDefaultPrevented()).toBe(true);
49
+
50
+ expect(delegateEvent.isPropagationStopped()).toBe(false);
51
+ delegateEvent.stopPropagation();
52
+ expect(delegateEvent.isPropagationStopped()).toBe(true);
53
+ });
54
+
55
+ it('should be able to `persist`', function() {
56
+ var delegateEvent = delegate({});
57
+
58
+ expect(delegateEvent.isPersistent()).toBe(false);
59
+ delegateEvent.persist();
60
+ expect(delegateEvent.isPersistent()).toBe(true);
61
+ });
62
+
63
+});
src/utils/PooledClass.js
+12
@@ -47,6 +47,17 @@ var twoArgumentPooler = function(a1, a2) {
47
}
48
};
49
50
+var threeArgumentPooler = function(a1, a2, a3) {
51
+ var Klass = this;
52
+ if (Klass.instancePool.length) {
53
+ var instance = Klass.instancePool.pop();
54
+ Klass.call(instance, a1, a2, a3);
55
+ return instance;
56
+ } else {
57
+ return new Klass(a1, a2, a3);
58
+ }
59
+};
60
+
61
var fiveArgumentPooler = function(a1, a2, a3, a4, a5) {
62
var Klass = this;
63
if (Klass.instancePool.length) {
@@ -95,6 +106,7 @@ var PooledClass = {
106
addPoolingTo: addPoolingTo,
107
oneArgumentPooler: oneArgumentPooler,
108
twoArgumentPooler: twoArgumentPooler,
109
+ threeArgumentPooler: threeArgumentPooler,
110
fiveArgumentPooler: fiveArgumentPooler
111
};
112