@samitouri / QOS-React-2 / commits / 3eaed5a122
Fork 0

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