├── LICENSE
├── NOTICES
├── README.md
├── actionflow.js
├── actionflow_test.js
├── actionflow_test_dom.html
├── actionlogger.js
├── cache.js
├── customeventdetail.js
├── customevents.js
├── customevents_test.js
├── customevents_test_dom.html
├── dispatcher.js
├── dispatcher_auto.js
├── dispatcher_example.js
├── dispatcher_export.js
├── dispatcher_test.js
├── dom.js
├── dom_test.js
├── event.js
├── event_test.js
├── eventcontract.js
├── eventcontract_auto.js
├── eventcontract_example.js
├── eventcontract_export.js
├── eventcontract_test.js
├── eventcontract_test_dom.html
├── generator.js
├── generator_test.js
├── generator_test_dom.html
├── jsaction.js
├── jsaction_test.js
├── jsaction_test_dom.html
├── loader.js
├── nativeevents.js
├── replay.js
├── replay_test.js
└── syntax.js
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/NOTICES:
--------------------------------------------------------------------------------
1 |
2 |
3 | jsaction may depend on portions of the following software:
4 |
5 |
6 | ===========
7 | closure_compiler, closure_library and closure are goverened by the following license:
8 |
9 | Apache License
10 | Version 2.0, January 2004
11 | http://www.apache.org/licenses/
12 |
13 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
14 |
15 | 1. Definitions.
16 |
17 | "License" shall mean the terms and conditions for use, reproduction,
18 | and distribution as defined by Sections 1 through 9 of this document.
19 |
20 | "Licensor" shall mean the copyright owner or entity authorized by
21 | the copyright owner that is granting the License.
22 |
23 | "Legal Entity" shall mean the union of the acting entity and all
24 | other entities that control, are controlled by, or are under common
25 | control with that entity. For the purposes of this definition,
26 | "control" means (i) the power, direct or indirect, to cause the
27 | direction or management of such entity, whether by contract or
28 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
29 | outstanding shares, or (iii) beneficial ownership of such entity.
30 |
31 | "You" (or "Your") shall mean an individual or Legal Entity
32 | exercising permissions granted by this License.
33 |
34 | "Source" form shall mean the preferred form for making modifications,
35 | including but not limited to software source code, documentation
36 | source, and configuration files.
37 |
38 | "Object" form shall mean any form resulting from mechanical
39 | transformation or translation of a Source form, including but
40 | not limited to compiled object code, generated documentation,
41 | and conversions to other media types.
42 |
43 | "Work" shall mean the work of authorship, whether in Source or
44 | Object form, made available under the License, as indicated by a
45 | copyright notice that is included in or attached to the work
46 | (an example is provided in the Appendix below).
47 |
48 | "Derivative Works" shall mean any work, whether in Source or Object
49 | form, that is based on (or derived from) the Work and for which the
50 | editorial revisions, annotations, elaborations, or other modifications
51 | represent, as a whole, an original work of authorship. For the purposes
52 | of this License, Derivative Works shall not include works that remain
53 | separable from, or merely link (or bind by name) to the interfaces of,
54 | the Work and Derivative Works thereof.
55 |
56 | "Contribution" shall mean any work of authorship, including
57 | the original version of the Work and any modifications or additions
58 | to that Work or Derivative Works thereof, that is intentionally
59 | submitted to Licensor for inclusion in the Work by the copyright owner
60 | or by an individual or Legal Entity authorized to submit on behalf of
61 | the copyright owner. For the purposes of this definition, "submitted"
62 | means any form of electronic, verbal, or written communication sent
63 | to the Licensor or its representatives, including but not limited to
64 | communication on electronic mailing lists, source code control systems,
65 | and issue tracking systems that are managed by, or on behalf of, the
66 | Licensor for the purpose of discussing and improving the Work, but
67 | excluding communication that is conspicuously marked or otherwise
68 | designated in writing by the copyright owner as "Not a Contribution."
69 |
70 | "Contributor" shall mean Licensor and any individual or Legal Entity
71 | on behalf of whom a Contribution has been received by Licensor and
72 | subsequently incorporated within the Work.
73 |
74 | 2. Grant of Copyright License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | copyright license to reproduce, prepare Derivative Works of,
78 | publicly display, publicly perform, sublicense, and distribute the
79 | Work and such Derivative Works in Source or Object form.
80 |
81 | 3. Grant of Patent License. Subject to the terms and conditions of
82 | this License, each Contributor hereby grants to You a perpetual,
83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
84 | (except as stated in this section) patent license to make, have made,
85 | use, offer to sell, sell, import, and otherwise transfer the Work,
86 | where such license applies only to those patent claims licensable
87 | by such Contributor that are necessarily infringed by their
88 | Contribution(s) alone or by combination of their Contribution(s)
89 | with the Work to which such Contribution(s) was submitted. If You
90 | institute patent litigation against any entity (including a
91 | cross-claim or counterclaim in a lawsuit) alleging that the Work
92 | or a Contribution incorporated within the Work constitutes direct
93 | or contributory patent infringement, then any patent licenses
94 | granted to You under this License for that Work shall terminate
95 | as of the date such litigation is filed.
96 |
97 | 4. Redistribution. You may reproduce and distribute copies of the
98 | Work or Derivative Works thereof in any medium, with or without
99 | modifications, and in Source or Object form, provided that You
100 | meet the following conditions:
101 |
102 | (a) You must give any other recipients of the Work or
103 | Derivative Works a copy of this License; and
104 |
105 | (b) You must cause any modified files to carry prominent notices
106 | stating that You changed the files; and
107 |
108 | (c) You must retain, in the Source form of any Derivative Works
109 | that You distribute, all copyright, patent, trademark, and
110 | attribution notices from the Source form of the Work,
111 | excluding those notices that do not pertain to any part of
112 | the Derivative Works; and
113 |
114 | (d) If the Work includes a "NOTICE" text file as part of its
115 | distribution, then any Derivative Works that You distribute must
116 | include a readable copy of the attribution notices contained
117 | within such NOTICE file, excluding those notices that do not
118 | pertain to any part of the Derivative Works, in at least one
119 | of the following places: within a NOTICE text file distributed
120 | as part of the Derivative Works; within the Source form or
121 | documentation, if provided along with the Derivative Works; or,
122 | within a display generated by the Derivative Works, if and
123 | wherever such third-party notices normally appear. The contents
124 | of the NOTICE file are for informational purposes only and
125 | do not modify the License. You may add Your own attribution
126 | notices within Derivative Works that You distribute, alongside
127 | or as an addendum to the NOTICE text from the Work, provided
128 | that such additional attribution notices cannot be construed
129 | as modifying the License.
130 |
131 | You may add Your own copyright statement to Your modifications and
132 | may provide additional or different license terms and conditions
133 | for use, reproduction, or distribution of Your modifications, or
134 | for any such Derivative Works as a whole, provided Your use,
135 | reproduction, and distribution of the Work otherwise complies with
136 | the conditions stated in this License.
137 |
138 | 5. Submission of Contributions. Unless You explicitly state otherwise,
139 | any Contribution intentionally submitted for inclusion in the Work
140 | by You to the Licensor shall be under the terms and conditions of
141 | this License, without any additional terms or conditions.
142 | Notwithstanding the above, nothing herein shall supersede or modify
143 | the terms of any separate license agreement you may have executed
144 | with Licensor regarding such Contributions.
145 |
146 | 6. Trademarks. This License does not grant permission to use the trade
147 | names, trademarks, service marks, or product names of the Licensor,
148 | except as required for reasonable and customary use in describing the
149 | origin of the Work and reproducing the content of the NOTICE file.
150 |
151 | 7. Disclaimer of Warranty. Unless required by applicable law or
152 | agreed to in writing, Licensor provides the Work (and each
153 | Contributor provides its Contributions) on an "AS IS" BASIS,
154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
155 | implied, including, without limitation, any warranties or conditions
156 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
157 | PARTICULAR PURPOSE. You are solely responsible for determining the
158 | appropriateness of using or redistributing the Work and assume any
159 | risks associated with Your exercise of permissions under this License.
160 |
161 | 8. Limitation of Liability. In no event and under no legal theory,
162 | whether in tort (including negligence), contract, or otherwise,
163 | unless required by applicable law (such as deliberate and grossly
164 | negligent acts) or agreed to in writing, shall any Contributor be
165 | liable to You for damages, including any direct, indirect, special,
166 | incidental, or consequential damages of any character arising as a
167 | result of this License or out of the use or inability to use the
168 | Work (including but not limited to damages for loss of goodwill,
169 | work stoppage, computer failure or malfunction, or any and all
170 | other commercial damages or losses), even if such Contributor
171 | has been advised of the possibility of such damages.
172 |
173 | 9. Accepting Warranty or Additional Liability. While redistributing
174 | the Work or Derivative Works thereof, You may choose to offer,
175 | and charge a fee for, acceptance of support, warranty, indemnity,
176 | or other liability obligations and/or rights consistent with this
177 | License. However, in accepting such obligations, You may act only
178 | on Your own behalf and on Your sole responsibility, not on behalf
179 | of any other Contributor, and only if You agree to indemnify,
180 | defend, and hold each Contributor harmless for any liability
181 | incurred by, or claims asserted against, such Contributor by reason
182 | of your accepting any such warranty or additional liability.
183 |
184 | END OF TERMS AND CONDITIONS
185 |
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JsAction has been migrated to the [angular/angular](https://github.com/angular/angular/blob/main/packages/core/primitives/event-dispatch/README.md) repository
2 |
3 |
4 | Please visit the angular/angular repository for its ongoing development and usage. This repository is archived and no longer actively maintained, it should not be used in development or production.
5 |
--------------------------------------------------------------------------------
/actionflow_test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2008 Google Inc. All rights reserved.
2 |
3 | /**
4 | */
5 |
6 | /** @suppress {extraProvide} */
7 | goog.provide('jsaction.ActionFlowTest');
8 | goog.setTestOnly('jsaction.ActionFlowTest');
9 |
10 | goog.require('goog.array');
11 | goog.require('goog.events');
12 | goog.require('goog.object');
13 | goog.require('goog.testing.MockClock');
14 | goog.require('goog.testing.MockControl');
15 | goog.require('goog.testing.jsunit');
16 | goog.require('jsaction');
17 | goog.require('jsaction.ActionFlow');
18 | goog.require('jsaction.Branch');
19 | /** @suppress {extraRequire} */
20 | goog.require('jsaction.replayEvent');
21 |
22 |
23 |
24 | var mockClock_;
25 | var mockControl_;
26 | var reportSent;
27 | var reportTimingData;
28 | var reportActionData;
29 | var savedGlobal_;
30 | var iframeDocument;
31 |
32 |
33 | function setUpPage() {
34 | var testHtml = '
' + document.body.innerHTML + '';
35 | var iframe = document.createElement('iframe');
36 | iframe.src = 'about:blank';
37 | document.body.appendChild(iframe);
38 | var doc = iframe.contentWindow.document;
39 | doc.open();
40 | doc.write(testHtml);
41 | doc.close();
42 | iframeDocument = doc;
43 | }
44 |
45 |
46 | function setUp() {
47 | mockControl_ = new goog.testing.MockControl();
48 | mockClock_ = new goog.testing.MockClock;
49 | mockClock_.install();
50 |
51 | goog.events.listen(
52 | jsaction.ActionFlow.report,
53 | jsaction.ActionFlow.EventType.DONE, reportHandler);
54 |
55 | reportSent = false;
56 | reportTimingData = {};
57 | reportActionData = {};
58 |
59 | savedGlobal_ = null;
60 | }
61 |
62 |
63 | function tearDown() {
64 | mockControl_.$tearDown();
65 | mockClock_.uninstall();
66 |
67 | if (savedGlobal_) {
68 | goog.global = savedGlobal_;
69 | }
70 | }
71 |
72 |
73 | function reportHandler(e) {
74 | reportSent = true;
75 | reportTimingData['flowType'] = e.flow.getType();
76 | reportTimingData['rtData'] = e.flow.timers();
77 | reportTimingData['cadData'] = e.flow.getExtraData();
78 |
79 | reportActionData = goog.object.clone(e.flow.getActionData());
80 | }
81 |
82 |
83 | var CONSTRUCTION_TIME = 314;
84 | var TICK_TIME = 415;
85 |
86 |
87 | function testActionFlow() {
88 | mockClock_.tick(CONSTRUCTION_TIME);
89 |
90 | var createdFlow = null;
91 | var creationListener = function(event) {
92 | createdFlow = event.flow;
93 | };
94 | goog.events.listen(
95 | jsaction.ActionFlow.report,
96 | jsaction.ActionFlow.EventType.CREATED,
97 | creationListener);
98 |
99 | var flow = new jsaction.ActionFlow('test');
100 | var timers = flow.timers();
101 | assertEquals(flow, createdFlow);
102 |
103 | var beforeReportTriggered = false;
104 | goog.events.listen(flow,
105 | jsaction.ActionFlow.EventType.BEFOREDONE, function() {
106 | assertFalse(reportSent);
107 | beforeReportTriggered = true;
108 | });
109 |
110 | mockClock_.tick(TICK_TIME);
111 |
112 | flow.tick('foo');
113 | assertEquals(1, timers.length);
114 | assertEquals('foo', timers[0][0]);
115 | assertEquals(TICK_TIME, timers[0][1]);
116 | assertEquals(TICK_TIME + CONSTRUCTION_TIME, flow.getTick('foo'));
117 | assertArrayEquals(['start', 'foo'], flow.getTickNames());
118 |
119 | flow.done(jsaction.Branch.MAIN);
120 | assertTrue(beforeReportTriggered);
121 | assertEquals('test', reportTimingData['flowType']);
122 | assertEquals(timers, reportTimingData['rtData']);
123 | }
124 |
125 |
126 | function testCadDataWithDataRecordedOnBeforeDone() {
127 | var flow = new jsaction.ActionFlow('test');
128 | var timers = flow.timers();
129 |
130 | var beforeReportTriggered = false;
131 | goog.events.listen(
132 | flow, jsaction.ActionFlow.EventType.BEFOREDONE, function(e) {
133 | e.flow.addExtraData('extra', 'foo');
134 | beforeReportTriggered = true;
135 | });
136 |
137 | var actionData = null;
138 | goog.events.listen(
139 | jsaction.ActionFlow.report, jsaction.ActionFlow.EventType.DONE,
140 | function() {
141 | actionData = flow.getActionData();
142 | });
143 |
144 |
145 | flow.addExtraData('bar', 'baz');
146 | flow.done(jsaction.Branch.MAIN);
147 |
148 | assertTrue(beforeReportTriggered);
149 | assertNotNull(actionData);
150 | assertEquals('bar:baz,extra:foo', actionData['cad']);
151 | }
152 |
153 |
154 | function testOverrideStartTime() {
155 | mockClock_.tick(CONSTRUCTION_TIME);
156 |
157 | var START_TIME = 1;
158 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
159 | var timers = flow.timers();
160 |
161 | mockClock_.tick(TICK_TIME);
162 |
163 | flow.tick('foo');
164 | assertEquals(1, timers.length);
165 | assertEquals('foo', timers[0][0]);
166 | assertEquals(TICK_TIME + CONSTRUCTION_TIME - START_TIME, timers[0][1]);
167 | assertEquals(TICK_TIME + CONSTRUCTION_TIME, flow.getTick('foo'));
168 | assertArrayEquals(['start', 'foo'], flow.getTickNames());
169 | }
170 |
171 |
172 | function testTickKeepsTimersSorted() {
173 | var START_TIME = 1;
174 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
175 | var timers = flow.timers();
176 |
177 | flow.tick('foo', {time: 10});
178 | flow.tick('bar', {time: 5});
179 | flow.tick('baz', {time: 20});
180 | flow.tick('boo', {time: 3});
181 | assertEquals(2, timers[0][1]);
182 | assertArrayEquals(['start', 'boo', 'bar', 'foo', 'baz'],
183 | flow.getTickNames());
184 | assertEquals(20, flow.getMaxTickTime());
185 | }
186 |
187 |
188 | function testTickNotInMaxTime() {
189 | var START_TIME = 1;
190 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
191 | var timers = flow.timers();
192 |
193 | flow.tick('foo', {time: 10});
194 | flow.tick('bar', {time: 5});
195 | flow.tick('baz', {time: 20});
196 | flow.tick('superbaz', {time: 200, doNotIncludeInMaxTime: true});
197 | flow.tick('boo', {time: 3});
198 | assertEquals(2, timers[0][1]);
199 | assertArrayEquals(['start', 'boo', 'bar', 'foo', 'baz', 'superbaz'],
200 | flow.getTickNames());
201 | assertEquals(20, flow.getMaxTickTime());
202 | }
203 |
204 |
205 | function testTickWithDoNotReportToServer() {
206 | var START_TIME = 1;
207 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
208 | var timers = flow.timers();
209 |
210 | flow.tick('foo');
211 | flow.tick('bar', {doNotReportToServer: true});
212 | assertEquals('foo', timers[0][0]);
213 | assertEquals(undefined, timers[0][2]);
214 | assertEquals('bar', timers[1][0]);
215 | assertEquals(true, timers[1][2]);
216 | }
217 |
218 |
219 | function testTickWithDoNotReportToServerDoesNotAffectMaxTickTime() {
220 | var START_TIME = 1;
221 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
222 | var timers = flow.timers();
223 |
224 | flow.tick('foo', {time: 10});
225 | flow.tick('bar', {time: 20, doNotReportToServer: true});
226 | assertEquals('foo', timers[0][0]);
227 | assertEquals(undefined, timers[0][2]);
228 | assertEquals('bar', timers[1][0]);
229 | assertEquals(true, timers[1][2]);
230 | assertEquals(10, flow.getMaxTickTime());
231 | assertNotNull(flow.getTick('foo'));
232 | assertNotNull(flow.getTick('bar'));
233 | assertEquals(20, flow.getTick('bar'));
234 | }
235 |
236 |
237 | function testNewBranchWithDoNotReportToServer() {
238 | var START_TIME = 1;
239 | var flow = new jsaction.ActionFlow('test', null, null, START_TIME);
240 | var timers = flow.timers();
241 |
242 | flow.tick('foo', {time: 10});
243 | flow.branch('branch1', 'bar0', {time: 20, doNotReportToServer: true});
244 | flow.done('branch1', 'bar1', {time: 30, doNotReportToServer: true});
245 | assertEquals(10, flow.getMaxTickTime());
246 | assertEquals('foo', timers[0][0]);
247 | assertEquals('bar0', timers[1][0]);
248 | assertEquals('bar1', timers[2][0]);
249 | }
250 |
251 |
252 | function testActionFlowAdoptDoesNothingOnNull() {
253 | var flow = new jsaction.ActionFlow('test');
254 | var timers = flow.timers();
255 |
256 | flow.adopt(null);
257 | assertEquals(0, timers.length);
258 | assertArrayEquals(['start'], flow.getTickNames());
259 | }
260 |
261 |
262 | function testActionFlowAdoptDoesNothingWithoutStart() {
263 | var flow = new jsaction.ActionFlow('test');
264 | var timers = flow.timers();
265 |
266 | flow.adopt({'foo': 10});
267 | assertEquals(0, timers.length);
268 | assertArrayEquals(['start'], flow.getTickNames());
269 | }
270 |
271 |
272 | function testActionFlowAdopt() {
273 | var flow = new jsaction.ActionFlow('test');
274 | var timers = flow.timers();
275 |
276 | flow.adopt({'start': 1, 'foo': 10});
277 | assertEquals(1, timers.length);
278 | assertEquals('foo', timers[0][0]);
279 | assertEquals(9, timers[0][1]);
280 | assertEquals(10, flow.getTick('foo'));
281 | assertArrayEquals(['start', 'foo'], flow.getTickNames());
282 | }
283 |
284 |
285 | function testActionFlowAdoptAtStartZero() {
286 | var flow = new jsaction.ActionFlow('test');
287 | var timers = flow.timers();
288 |
289 | flow.adopt({'start': 0, 'foo': 10});
290 | assertEquals(1, timers.length);
291 | assertEquals(10, flow.getTick('foo'));
292 | assertArrayEquals(['start', 'foo'], flow.getTickNames());
293 | }
294 |
295 |
296 | function testActionFlowAdoptDone() {
297 | var flow = new jsaction.ActionFlow('test');
298 | flow.adopt({'start': 1, 'foo': 10});
299 | flow.done(jsaction.Branch.MAIN);
300 | assertTrue('Adopt sends report with one done.', reportSent);
301 | }
302 |
303 |
304 | function testActionFlowAdoptDoneWithExpect() {
305 | var flow = new jsaction.ActionFlow('test');
306 | var mockBranches = {'branch1': 2};
307 | mockBranches[jsaction.Branch.MAIN] = 1;
308 | flow.adopt({'start': 1, 'foo': 10}, mockBranches);
309 | flow.done('branch1');
310 | assertFalse('Report incorrectly sent.', reportSent);
311 | flow.done(jsaction.Branch.MAIN);
312 | assertFalse('Report incorrectly sent.', reportSent);
313 |
314 | flow.done('branch1');
315 | assertTrue('Report not sent after the flow finished.', reportSent);
316 | }
317 |
318 |
319 | function testActionFlowMerge() {
320 | var flow = new jsaction.ActionFlow('test', null, null, 3);
321 | flow.tick('bar', {time: 20});
322 | var timers = flow.timers();
323 |
324 | jsaction.ActionFlow.merge(
325 | flow, {'start': 1, 'foo': 10, 'baz': 5, 'boo': 30});
326 | assertEquals(4, timers.length);
327 | assertEquals(2, timers[0][1]);
328 | assertEquals(7, timers[1][1]);
329 | assertEquals(10, flow.getTick('foo'));
330 | assertEquals(20, flow.getTick('bar'));
331 | assertArrayEquals(['start', 'baz', 'foo', 'bar', 'boo'],
332 | flow.getTickNames());
333 | }
334 |
335 |
336 | function testReportSendCalledWithoutTicks() {
337 | var flow = new jsaction.ActionFlow('test');
338 | flow.done(jsaction.Branch.MAIN);
339 | assertTrue('ActionFlow reported without ticks.', reportSent);
340 | }
341 |
342 |
343 | function testTickWithoutDoneDoesNotSendReport() {
344 | var flow = new jsaction.ActionFlow('test');
345 | flow.tick('foo');
346 | assertFalse('Tick never sends report.', reportSent);
347 | }
348 |
349 |
350 | function testActionFlowWithWeirdFlowNames() {
351 | var flow = new jsaction.ActionFlow('t&s$tf#b~c');
352 | flow.tick('foo');
353 | flow.done(jsaction.Branch.MAIN);
354 |
355 | assertEquals('t_s$tf#b_c', reportTimingData['flowType']);
356 | }
357 |
358 |
359 | function testOneBranch() {
360 | var flow = new jsaction.ActionFlow('test');
361 | flow.tick('foo');
362 |
363 | flow.branch('branch1');
364 | assertFalse(reportSent);
365 |
366 | flow.done('branch1');
367 | assertFalse(reportSent);
368 |
369 | flow.done(jsaction.Branch.MAIN);
370 | assertTrue(reportSent);
371 |
372 | assertEquals(1, reportTimingData['rtData'].length);
373 | }
374 |
375 |
376 | function testMultipleBranches() {
377 | var flow = new jsaction.ActionFlow('test');
378 | flow.tick('foo');
379 |
380 | flow.branch('branch1');
381 | flow.branch('branch2');
382 |
383 | flow.done('branch1');
384 | assertFalse(reportSent);
385 |
386 | flow.done(jsaction.Branch.MAIN);
387 | assertFalse(reportSent);
388 |
389 | flow.done('branch2');
390 | assertTrue(reportSent);
391 |
392 | assertEquals(1, reportTimingData['rtData'].length);
393 | }
394 |
395 |
396 | function testTickDoneShortcut() {
397 | var flow = new jsaction.ActionFlow('test');
398 | flow.done(jsaction.Branch.MAIN, 'bar');
399 | assertTrue(reportSent);
400 | assertEquals(1, reportTimingData['rtData'].length);
401 | }
402 |
403 |
404 | function testTickBranchShortcut() {
405 | var flow = new jsaction.ActionFlow('test');
406 |
407 | flow.branch('foobranch', 'footick');
408 | flow.done('foobranch', 'bartick');
409 | assertFalse(reportSent);
410 |
411 | flow.done(jsaction.Branch.MAIN, 'baz');
412 | assertTrue(reportSent);
413 | assertEquals(3, reportTimingData['rtData'].length);
414 | }
415 |
416 |
417 | function testTrackedCallback() {
418 | var flow = new jsaction.ActionFlow('test');
419 | var callbackCalled = false;
420 | var fn = function() {
421 | callbackCalled = true;
422 | };
423 |
424 | var trackedCallback = flow.callback(fn, 'testbranch', 't0', 't1');
425 |
426 | assertTrue(flow.getTick('t0') !== undefined);
427 |
428 | jsaction.ActionFlow.done(flow, jsaction.Branch.MAIN);
429 | assertFalse(reportSent);
430 |
431 | trackedCallback();
432 |
433 | assertTrue(flow.getTick('t1') !== undefined);
434 | assertTrue(callbackCalled);
435 | assertTrue(reportSent);
436 | }
437 |
438 |
439 | function testTrackedCallbackThrows() {
440 | var flow = new jsaction.ActionFlow('test');
441 | var callbackCalled = false;
442 | var fn = function() {
443 | callbackCalled = true;
444 | throw 'foo';
445 | };
446 |
447 | var trackedCallback = flow.callback(fn, 'testbranch', 't0', 't1');
448 |
449 | assertTrue(flow.getTick('t0') !== undefined);
450 |
451 | jsaction.ActionFlow.done(flow, jsaction.Branch.MAIN);
452 | assertFalse(reportSent);
453 |
454 | try {
455 | trackedCallback();
456 | } catch (e) {
457 | assertEquals('foo', e);
458 | }
459 |
460 | assertTrue(flow.getTick('t1') !== undefined);
461 | assertTrue(callbackCalled);
462 | assertTrue(reportSent);
463 | }
464 |
465 |
466 | function testIsOfType() {
467 | var flow = new jsaction.ActionFlow('foo');
468 | assertTrue(flow.isOfType('foo'));
469 | assertFalse(flow.isOfType('bar'));
470 | }
471 |
472 |
473 | function testIsOfTypeWithWeirdNames() {
474 | var flow = new jsaction.ActionFlow('t&est');
475 | assertTrue(flow.isOfType('t&est'));
476 | assertTrue(flow.isOfType('t_est'));
477 | assertFalse(flow.isOfType('bar'));
478 | }
479 |
480 | function testSetEventId() {
481 | var flow = new jsaction.ActionFlow('test');
482 | flow.maybeSetEventId('abcdefg');
483 |
484 | // No-op: already set.
485 | flow.maybeSetEventId('other-event-id');
486 |
487 | flow.done(jsaction.Branch.MAIN, 'done');
488 |
489 | assertTrue(reportSent);
490 | assertEquals('abcdefg', reportActionData['ei']);
491 | }
492 |
493 | function testAddActionDataWithTimers() {
494 | var flow = new jsaction.ActionFlow('test');
495 | flow.addExtraData('key1', 'value1');
496 | flow.addExtraData('key2', 'value2');
497 | flow.done(jsaction.Branch.MAIN, 'done');
498 |
499 | assertTrue(reportSent);
500 | assertEquals(reportTimingData['cadData']['key1'], 'value1');
501 | assertEquals(reportTimingData['cadData']['key2'], 'value2');
502 | }
503 |
504 |
505 | function testDuplicateTicks() {
506 | var flow = new jsaction.ActionFlow('test');
507 | flow.tick('tick');
508 | flow.tick('tick');
509 | flow.done(jsaction.Branch.MAIN);
510 | assertTrue(reportSent);
511 | assertEquals('tick', reportTimingData['cadData']['dup']);
512 | }
513 |
514 |
515 | function testMultipleDuplicateTicks() {
516 | var flow = new jsaction.ActionFlow('test');
517 | flow.tick('tick1');
518 | flow.tick('tick1');
519 | flow.tick('tick2');
520 | flow.tick('tick2');
521 | flow.tick('tick1');
522 | flow.tick('tick3');
523 | flow.done(jsaction.Branch.MAIN);
524 | assertTrue(reportSent);
525 | assertEquals('tick1|tick2', reportTimingData['cadData']['dup']);
526 | }
527 |
528 |
529 | function testAction() {
530 | var flow = new jsaction.ActionFlow('barAction');
531 | var target = document.getElementById('bar2');
532 | flow.action(target);
533 | flow.done(jsaction.Branch.MAIN);
534 | assertTrue(reportSent);
535 | assertEquals('barAction', reportActionData['ct']);
536 | assertEquals(1, reportActionData['cd']);
537 | assertEquals('oi:maps.foo.bar', reportActionData['cad']);
538 | }
539 |
540 |
541 | function testAction2() {
542 | var flow = new jsaction.ActionFlow('barAction');
543 | var target = document.getElementById('bar3');
544 | flow.action(target);
545 | flow.done(jsaction.Branch.MAIN);
546 | assertTrue(reportSent);
547 | assertEquals('barAction', reportActionData['ct']);
548 | assertEquals(2, reportActionData['cd']);
549 | assertEquals('oi:maps.foo.bar', reportActionData['cad']);
550 | }
551 |
552 |
553 | function testActionWithoutOiData() {
554 | var flow = new jsaction.ActionFlow('barAction');
555 | var target = document.getElementById('foo2');
556 | flow.action(target);
557 | flow.done(jsaction.Branch.MAIN);
558 | assertTrue(reportSent);
559 | assertEquals('barAction', reportActionData['ct']);
560 | }
561 |
562 |
563 | function testActionAcrossIframes() {
564 | var flow = new jsaction.ActionFlow('barAction');
565 | var target = iframeDocument.getElementById('bar2');
566 | flow.action(target);
567 | flow.done(jsaction.Branch.MAIN);
568 | assertTrue(reportSent);
569 | assertEquals('barAction', reportActionData['ct']);
570 | assertEquals(1, reportActionData['cd']);
571 | assertEquals('oi:maps.foo.bar', reportActionData['cad']);
572 | }
573 |
574 |
575 | function testActionFromConstructor() {
576 | // When the constructor is passed a node and a click event, action() is
577 | // triggered from within the constructor.
578 | var target = document.getElementById('bar2');
579 | var clickEvent = jsaction.createEvent({type: 'click'});
580 | var flow = new jsaction.ActionFlow('barAction', target, clickEvent);
581 | flow.done(jsaction.Branch.MAIN);
582 | assertEquals('barAction', reportActionData['ct']);
583 | assertEquals(1, reportActionData['cd']);
584 | assertEquals('oi:maps.foo.bar', reportActionData['cad']);
585 | }
586 |
587 |
588 | function testActionNestedEi() {
589 | var flow = new jsaction.ActionFlow('nestedAction');
590 | var target = document.getElementById('nested');
591 | flow.action(target);
592 | flow.done(jsaction.Branch.MAIN);
593 | assertTrue(reportSent);
594 | assertEquals('eventid2', reportActionData['ei']);
595 | assertFalse('ved' in reportActionData);
596 | }
597 |
598 |
599 | function testActionWithNoTracking() {
600 | var flow = new jsaction.ActionFlow('fooAction');
601 | var target = document.getElementById('foo1');
602 | flow.action(target);
603 | flow.done(jsaction.Branch.MAIN);
604 | assertEquals(undefined, reportActionData['ct']);
605 | assertEquals(undefined, reportActionData['cd']);
606 | assertEquals(undefined, reportActionData['cad']);
607 | assertEquals(undefined, reportActionData['ei']);
608 | assertEquals(undefined, reportActionData['ved']);
609 | }
610 |
611 |
612 | function testActionWithNoOi() {
613 | var flow = new jsaction.ActionFlow('fooAction');
614 | var target = document.getElementById('foo2');
615 | flow.action(target);
616 | flow.done(jsaction.Branch.MAIN);
617 | assertTrue(reportSent);
618 | assertEquals('fooAction', reportActionData['ct']);
619 | assertEquals(undefined, reportActionData['cd']);
620 | assertEquals(undefined, reportActionData['cad']);
621 | }
622 |
623 |
624 | function testActionWithVed() {
625 | var flow = new jsaction.ActionFlow('bazAction');
626 | var target = document.getElementById('baz');
627 | flow.action(target);
628 | flow.done(jsaction.Branch.MAIN);
629 | assertTrue(reportSent);
630 | assertEquals('bazAction', reportActionData['ct']);
631 | assertEquals(0, reportActionData['cd']);
632 | assertEquals('oi:maps2.baz2.baz3', reportActionData['cad']);
633 | assertEquals('baz1', reportActionData['ved']);
634 | assertEquals('eventid', reportActionData['ei']);
635 | }
636 |
637 |
638 | function testActionWithVet() {
639 | var flow = new jsaction.ActionFlow('nestedAction');
640 | var target = document.getElementById('nested');
641 | flow.action(target);
642 | flow.done(jsaction.Branch.MAIN);
643 | assertTrue(reportSent);
644 | assertEquals('nestedAction', reportActionData['ct']);
645 | assertEquals('vet2', reportActionData['vet']);
646 | }
647 |
648 |
649 | function testActionWithVetNoJstrack() {
650 | var flow = new jsaction.ActionFlow('fooAction');
651 | var target = document.getElementById('foo1');
652 | flow.action(target);
653 | flow.done(jsaction.Branch.MAIN);
654 | assertTrue(reportSent);
655 | assertEquals('vet1', reportActionData['vet']);
656 | }
657 |
658 |
659 | function testAddActionData() {
660 | var flow = new jsaction.ActionFlow('barAction');
661 | var target = document.getElementById('bar2');
662 | flow.action(target);
663 | flow.addExtraData('key1', 'value1');
664 | assertEquals('value1', flow.getExtraData()['key1']);
665 | flow.addExtraData('key2', 'value2');
666 | assertEquals('value2', flow.getExtraData()['key2']);
667 | flow.done(jsaction.Branch.MAIN);
668 | assertTrue(reportSent);
669 | assertEquals('oi:maps.foo.bar,key1:value1,key2:value2',
670 | reportActionData['cad']);
671 | }
672 |
673 |
674 | function testStaticTickBranchDone() {
675 | var undefinedFlow = undefined;
676 | jsaction.ActionFlow.tick(undefinedFlow, 'foo');
677 | jsaction.ActionFlow.branch(undefinedFlow, 'branchfoo');
678 | jsaction.ActionFlow.done(undefinedFlow, 'branchfoo');
679 |
680 | var flow = new jsaction.ActionFlow('test');
681 | var timers = flow.timers();
682 |
683 | jsaction.ActionFlow.tick(flow, 'tick', 0);
684 | assertEquals(0, flow.getTick('tick'));
685 |
686 | jsaction.ActionFlow.branch(flow, 'testbranch', 'branchtick');
687 | assertTrue(flow.getTick('tick') !== undefined);
688 |
689 | flow.done('testbranch');
690 | assertFalse(reportSent);
691 |
692 | jsaction.ActionFlow.done(flow, jsaction.Branch.MAIN, 'done');
693 | assertEquals('start_tick_branchtick_done', flow.getTickNames().join('_'));
694 | assertTrue(reportSent);
695 | }
696 |
697 |
698 | function testAbandonActionFlow() {
699 | mockClock_.tick(CONSTRUCTION_TIME);
700 |
701 | var flow = new jsaction.ActionFlow('test');
702 | var timers = flow.timers();
703 |
704 | var beforeReportTriggered = false;
705 | goog.events.listen(jsaction.ActionFlow.report,
706 | jsaction.ActionFlow.EventType.BEFOREDONE, function() {
707 | assertFalse(reportSent);
708 | beforeReportTriggered = true;
709 | });
710 | var abandonedTriggered = false;
711 | goog.events.listen(flow,
712 | jsaction.ActionFlow.EventType.ABANDONED, function() {
713 | abandonedTriggered = true;
714 | });
715 |
716 | mockClock_.tick(TICK_TIME);
717 |
718 | flow.tick('foo');
719 | assertEquals(1, timers.length);
720 | assertEquals('foo', timers[0][0]);
721 | flow.abandon();
722 |
723 | flow.done(jsaction.Branch.MAIN);
724 | assertTrue(abandonedTriggered);
725 | assertFalse(beforeReportTriggered);
726 | assertFalse(reportSent);
727 | assertUndefined(reportTimingData['flowType']);
728 | assertUndefined(reportTimingData['rtData']);
729 | assertUndefined(reportTimingData['cadData']);
730 | }
731 |
732 |
733 | function testGetType() {
734 | var flow = new jsaction.ActionFlow('test');
735 | assertEquals('test', flow.getType());
736 | }
737 |
738 |
739 | function testSetType() {
740 | var flow = new jsaction.ActionFlow('foo');
741 | assertEquals('foo', flow.getType());
742 | assertEquals('foo', flow.flowType());
743 | flow.setType('bar');
744 | assertEquals('bar', flow.getType());
745 | assertEquals('bar', flow.flowType());
746 | }
747 |
748 |
749 | function testErrorReportTriggeredOnBranchAfterFlowFinished() {
750 | var flow = new jsaction.ActionFlow('test');
751 | var errorEvent = null;
752 |
753 | goog.events.listen(jsaction.ActionFlow.report,
754 | jsaction.ActionFlow.EventType.ERROR, function(e) {
755 | errorEvent = e;
756 | });
757 |
758 | flow.tick('foo');
759 | flow.addExtraData('key1', 'value1');
760 |
761 | flow.done(jsaction.Branch.MAIN);
762 | assertNull(errorEvent);
763 |
764 | flow.branch('wrongbranch');
765 | assertNotNull(errorEvent);
766 |
767 | assertEquals(jsaction.ActionFlow.Error.BRANCH, errorEvent.error);
768 | assertEquals('wrongbranch', errorEvent.branch);
769 | assertEquals('test', errorEvent.flow.flowType());
770 | assertTrue(errorEvent.finished);
771 | assertEquals(reportTimingData['rtData'], errorEvent.flow.timers());
772 | assertEquals(reportTimingData['cadData'], errorEvent.flow.getExtraData());
773 | }
774 |
775 |
776 | function testErrorReportTriggeredOnDoneAfterFlowFinished() {
777 | var flow = new jsaction.ActionFlow('test');
778 | var errorEvent = null;
779 |
780 | goog.events.listen(jsaction.ActionFlow.report,
781 | jsaction.ActionFlow.EventType.ERROR, function(e) {
782 | errorEvent = e;
783 | });
784 |
785 | flow.tick('foo');
786 | flow.addExtraData('key1', 'value1');
787 |
788 | flow.done(jsaction.Branch.MAIN);
789 | assertNull(errorEvent);
790 |
791 | flow.done('wrongbranch', 'badtick');
792 | assertNotNull(errorEvent);
793 |
794 | assertEquals(jsaction.ActionFlow.Error.DONE, errorEvent.error);
795 | assertEquals('wrongbranch', errorEvent.branch);
796 | assertTrue(errorEvent.finished);
797 | assertEquals('badtick', errorEvent.tick);
798 | assertEquals('test', errorEvent.flow.flowType());
799 | assertEquals(reportTimingData['rtData'], errorEvent.flow.timers());
800 | assertEquals(reportTimingData['cadData'], errorEvent.flow.getExtraData());
801 | }
802 |
803 |
804 | function testErrorReportTriggeredOnTickAfterFlowFinished() {
805 | var flow = new jsaction.ActionFlow('errortest');
806 | var errorEvent = null;
807 |
808 | goog.events.listen(jsaction.ActionFlow.report,
809 | jsaction.ActionFlow.EventType.ERROR, function(e) {
810 | errorEvent = e;
811 | });
812 |
813 | flow.done(jsaction.Branch.MAIN);
814 | assertNull(errorEvent);
815 |
816 | flow.tick('badtick');
817 | assertNotNull(errorEvent);
818 |
819 | assertEquals(jsaction.ActionFlow.Error.TICK, errorEvent.error);
820 | assertUndefined(errorEvent.branch);
821 | assertEquals('badtick', errorEvent.tick);
822 | assertTrue(errorEvent.finished);
823 | assertEquals('errortest', errorEvent.flow.flowType());
824 | assertEquals(reportTimingData['rtData'], errorEvent.flow.timers());
825 | assertEquals(reportTimingData['cadData'], errorEvent.flow.getExtraData());
826 | assertTrue(goog.object.isEmpty(errorEvent.flow.branches()));
827 | }
828 |
829 |
830 | function testErrorReportTriggeredOnAddExtraDataAfterFlowFinished() {
831 | var flow = new jsaction.ActionFlow('errortest');
832 | var errorEvent = null;
833 |
834 | goog.events.listen(jsaction.ActionFlow.report,
835 | jsaction.ActionFlow.EventType.ERROR, function(e) {
836 | errorEvent = e;
837 | });
838 |
839 | flow.done(jsaction.Branch.MAIN);
840 | assertNull(errorEvent);
841 |
842 | flow.addExtraData('badkey', 'bad value');
843 | assertNotNull(errorEvent);
844 |
845 | assertEquals(jsaction.ActionFlow.Error.EXTRA_DATA, errorEvent.error);
846 | assertUndefined(errorEvent.branch);
847 | assertUndefined(errorEvent.tick);
848 | assertTrue(errorEvent.finished);
849 | assertEquals('errortest', errorEvent.flow.flowType());
850 | assertTrue(goog.object.isEmpty(errorEvent.flow.branches()));
851 | }
852 |
853 |
854 | function testErrorReportTriggeredOnActionAfterFlowFinished() {
855 | var target = document.getElementById('bar2');
856 | var flow = new jsaction.ActionFlow('errortest');
857 | var errorEvent = null;
858 |
859 | goog.events.listen(jsaction.ActionFlow.report,
860 | jsaction.ActionFlow.EventType.ERROR, function(e) {
861 | if (!errorEvent) {
862 | errorEvent = e;
863 | }
864 | });
865 |
866 | flow.done(jsaction.Branch.MAIN);
867 | assertNull(errorEvent);
868 |
869 | flow.action(target);
870 | assertNotNull(errorEvent);
871 |
872 | assertEquals(jsaction.ActionFlow.Error.ACTION, errorEvent.error);
873 | assertUndefined(errorEvent.branch);
874 | assertUndefined(errorEvent.tick);
875 | assertTrue(errorEvent.finished);
876 | assertEquals('errortest', errorEvent.flow.flowType());
877 | assertTrue(goog.object.isEmpty(errorEvent.flow.branches()));
878 | }
879 |
880 |
881 | function testErrorReportTriggeredOnDoneOnABranchNotPending() {
882 | var flow = new jsaction.ActionFlow('errortest');
883 | var errorEvent = null;
884 |
885 | goog.events.listen(jsaction.ActionFlow.report,
886 | jsaction.ActionFlow.EventType.ERROR, function(e) {
887 | errorEvent = e;
888 | });
889 |
890 | flow.branch('branch1');
891 | // branch2 was never opened.
892 | flow.done('branch2');
893 | flow.done(jsaction.Branch.MAIN);
894 |
895 | assertNotNull(errorEvent);
896 |
897 | assertEquals(jsaction.ActionFlow.Error.DONE, errorEvent.error);
898 | assertEquals('branch2', errorEvent.branch);
899 | assertUndefined(errorEvent.tick);
900 | assertFalse(errorEvent.finished);
901 | assertEquals('errortest', errorEvent.flow.flowType());
902 | assertEquals(1, errorEvent.flow.branches()['branch1']);
903 | }
904 |
905 |
906 | function testJsActionFlow_Type_Node_Event_Values() {
907 | var node = document.createElement('div');
908 | node.foo = 1;
909 | var event = jsaction.createEvent({type: 'click'});
910 | var flowType = 'bar';
911 | var flow = new jsaction.ActionFlow(flowType, node, event);
912 |
913 | assertEquals(flowType, flow.flowType());
914 | assertEquals(1, flow.value('foo'));
915 | assertEquals(node, flow.node());
916 | }
917 |
918 |
919 | function testGetEventNodeNull() {
920 | var flow = new jsaction.ActionFlow('baz', null, null);
921 | assertNull(flow.event());
922 | assertNull(flow.node());
923 | }
924 |
925 |
926 | function testDoneClearsNodeAndEvent() {
927 | var node = document.createElement('div');
928 | var event = jsaction.createEvent({type: 'click'});
929 | var flow = new jsaction.ActionFlow('baz', node, event);
930 |
931 | assertNotNull(flow.node());
932 | assertNotNull(flow.event());
933 |
934 | flow.done(jsaction.Branch.MAIN);
935 |
936 | assertNull(flow.node());
937 | assertNull(flow.event());
938 | }
939 |
940 |
941 | function testDoneClearsNodeAndEvent_MultipleBranches() {
942 | var node = document.createElement('div');
943 | var event = jsaction.createEvent({type: 'click'});
944 | var flow = new jsaction.ActionFlow('test', node, event);
945 |
946 | assertNotNull(flow.node());
947 | assertNotNull(flow.event());
948 |
949 | flow.branch('b1');
950 | flow.branch('b2');
951 |
952 | flow.done('b1');
953 |
954 | assertNotNull(flow.node());
955 | assertNotNull(flow.event());
956 |
957 | flow.done('b2');
958 |
959 | assertNotNull(flow.node_);
960 | assertNotNull(flow.event_);
961 |
962 | flow.done(jsaction.Branch.MAIN);
963 |
964 | assertNull(flow.node());
965 | assertNull(flow.event());
966 | }
967 |
968 |
969 | function testJsActionFlowCopiesEventObjectOnIEBefore9() {
970 | // This is restored on tearDown so that it doesn't affect other tests if this
971 | // one fails.
972 | savedGlobal_ = goog.global;
973 | // The event gets copied if there is no document.createEvent and we have
974 | // document.createEventObject (see jsaction.event.maybeCopyEvent).
975 | var mockDocument = {};
976 | mockDocument.createEventObject = function() {
977 | var retval = {};
978 | for (var i in event) {
979 | retval[i] = event[i];
980 | }
981 | return retval;
982 | };
983 | goog.global = {};
984 | goog.global['document'] = mockDocument;
985 | var node = {};
986 | var event = {'type': '', 'foo': {}};
987 | var flow = new jsaction.ActionFlow('foo', node, event);
988 |
989 | // The event should be deep copied.
990 | assertNotEquals(event, flow.event());
991 | assertEquals(event['foo'], flow.event()['foo']);
992 | }
993 |
994 |
995 | function testGetNamespace() {
996 | var node = document.createElement('div');
997 | var event = jsaction.createEvent({type: 'click'});
998 |
999 | var flowWithNamespace = new jsaction.ActionFlow('gna.fu', node, event);
1000 | assertEquals('gna', flowWithNamespace.actionNamespace());
1001 |
1002 | var flowWithoutNamespace = new jsaction.ActionFlow('fu', node, event);
1003 | assertEquals('', flowWithoutNamespace.actionNamespace());
1004 | }
1005 |
1006 |
1007 | function testInstanceRegistry() {
1008 | var flow = new jsaction.ActionFlow('foo');
1009 | assertTrue(goog.array.contains(jsaction.ActionFlow.instances, flow));
1010 |
1011 | flow.done(jsaction.Branch.MAIN);
1012 | assertFalse(goog.array.contains(jsaction.ActionFlow.instances, flow));
1013 | }
1014 |
1015 | function testGetDelayForReactive() {
1016 | var node = document.createElement('div');
1017 | node.foo = 1;
1018 | var event = jsaction.createEvent({type: 'click'});
1019 | event.originalTimestamp = 1;
1020 | var flow = new jsaction.ActionFlow('test', node, event);
1021 | assertEquals(event.timeStamp - event.originalTimestamp, flow.getDelay());
1022 | }
1023 |
1024 | function testGetDelayForWiz() {
1025 | var node = document.createElement('div');
1026 | node.foo = 1;
1027 | var event = jsaction.createEvent({type: 'click'});
1028 | event.originalTimestamp = 100;
1029 | const delay = 300;
1030 | var mockGetTimestamp = mockControl_.createMethodMock(
1031 | jsaction.ActionFlow, 'getTimestamp_');
1032 | mockGetTimestamp().$returns(event.originalTimestamp + delay).$anyTimes();
1033 | var flow = new jsaction.ActionFlow('test', node, event);
1034 | flow.setWiz();
1035 |
1036 | mockControl_.$replayAll();
1037 | assertEquals(delay, flow.getDelay());
1038 | mockControl_.$verifyAll();
1039 | }
1040 |
--------------------------------------------------------------------------------
/actionflow_test_dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bar1
6 |
7 |
8 | bar2
9 |
10 |
11 | bar3
12 |
13 |
14 |
15 |
16 |
23 |
30 |
37 |
45 |
--------------------------------------------------------------------------------
/actionlogger.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Interface for a logger used to log user interaction via
3 | * jsactions.
4 | */
5 | goog.provide('jsaction.ActionLogger');
6 |
7 | goog.requireType('jsaction.ActionFlow');
8 |
9 | goog.scope(function() {
10 |
11 |
12 |
13 | /**
14 | * Creates a no-op ActionLogger.
15 | *
16 | * @constructor
17 | */
18 | jsaction.ActionLogger = function() {};
19 |
20 | /**
21 | * Logs when an action is actually dispatched. Should be invoked by handler
22 | * before the action is actually handled.
23 | *
24 | * @param {!jsaction.ActionFlow} actionFlow The action flow for the action.
25 | * @param {string=} opt_info optional string to identify information on
26 | * the controller that handles the action.
27 | */
28 | jsaction.ActionLogger.prototype.logDispatch = function(actionFlow, opt_info) {};
29 |
30 | }); // goog.scope
31 |
--------------------------------------------------------------------------------
/cache.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc. All Rights Reserved.
2 |
3 | /**
4 | * @fileoverview Implements both a per element cache of its jsaction mapping
5 | * and a global parse cache. The former avoids an attribute access per DOM node
6 | * and the the latter avoids parsing the same jsaction annotation twice. In
7 | * a typical application the same jsaction value would be used many times while
8 | * the overall number of different values should be relatively small.
9 | */
10 |
11 |
12 | goog.provide('jsaction.Cache');
13 |
14 |
15 | goog.require('jsaction.Property');
16 |
17 |
18 | /**
19 | * Map from jsaction annotation to a parsed map from event name to action name.
20 | * @private @const {!Object>}
21 | */
22 | jsaction.Cache.parseCache_ = {};
23 |
24 |
25 |
26 | /**
27 | * Reads the jsaction parser cache from the given DOM Element.
28 | *
29 | * @param {!Element} element .
30 | * @return {!Object.} Map from event to qualified name
31 | * of the jsaction bound to it.
32 | */
33 | jsaction.Cache.get = function(element) {
34 | return element[jsaction.Property.JSACTION];
35 | };
36 |
37 |
38 | /**
39 | * Writes the jsaction parser cache to the given DOM Element.
40 | *
41 | * @param {!Element} element .
42 | * @param {!Object.} actionMap Map from event to
43 | * qualified name of the jsaction bound to it.
44 | */
45 | jsaction.Cache.set = function(element, actionMap) {
46 | element[jsaction.Property.JSACTION] = actionMap;
47 | };
48 |
49 |
50 | /**
51 | * Looks up the parsed action map from the source jsaction attribute value.
52 | *
53 | * @param {string} text Unparsed jsaction attribute value.
54 | * @return {!Object.|undefined} Parsed jsaction attribute value,
55 | * if already present in the cache.
56 | */
57 | jsaction.Cache.getParsed = function(text) {
58 | return jsaction.Cache.parseCache_[text];
59 | };
60 |
61 |
62 | /**
63 | * Inserts the parse result for the given source jsaction value into the cache.
64 | *
65 | * @param {string} text Unparsed jsaction attribute value.
66 | * @param {!Object.} parsed Attribute value parsed into the
67 | * action map.
68 | */
69 | jsaction.Cache.setParsed = function(text, parsed) {
70 | jsaction.Cache.parseCache_[text] = parsed;
71 | };
72 |
73 |
74 | /**
75 | * Clears the jsaction parser cache from the given DOM Element.
76 | *
77 | * @param {!Element} element .
78 | */
79 | jsaction.Cache.clear = function(element) {
80 | if (jsaction.Property.JSACTION in element) {
81 | delete element[jsaction.Property.JSACTION];
82 | }
83 | };
84 |
85 |
86 | /**
87 | * Reads the cached jsaction namespace from the given DOM
88 | * Element. Undefined means there is no cached value; null is a cached
89 | * jsnamespace attribute that's absent.
90 | *
91 | * @param {!Element} element .
92 | * @return {string|null|undefined} .
93 | */
94 | jsaction.Cache.getNamespace = function(element) {
95 | return element[jsaction.Property.JSNAMESPACE];
96 | };
97 |
98 |
99 | /**
100 | * Writes the cached jsaction namespace to the given DOM Element. Null
101 | * represents a jsnamespace attribute that's absent.
102 | *
103 | * @param {!Element} element .
104 | * @param {?string} jsnamespace .
105 | */
106 | jsaction.Cache.setNamespace = function(element, jsnamespace) {
107 | element[jsaction.Property.JSNAMESPACE] = jsnamespace;
108 | };
109 |
110 |
111 | /**
112 | * Clears the cached jsaction namespace from the given DOM Element.
113 | *
114 | * @param {!Element} element .
115 | */
116 | jsaction.Cache.clearNamespace = function(element) {
117 | if (jsaction.Property.JSNAMESPACE in element) {
118 | delete element[jsaction.Property.JSNAMESPACE];
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/customeventdetail.js:
--------------------------------------------------------------------------------
1 | goog.module('jsaction.CustomEventDetail');
2 |
3 | /**
4 | * @record
5 | * @template T
6 | */
7 | exports = class {
8 | constructor() {
9 | /** @type {string} */
10 | this.type;
11 |
12 | /** @type {T} */
13 | this.data;
14 |
15 | /** @type {!Event|undefined} */
16 | this.triggerEvent;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/customevents.js:
--------------------------------------------------------------------------------
1 | goog.provide('jsaction.testing.CustomEvents');
2 | goog.setTestOnly('jsaction.testing.CustomEvents');
3 |
4 | goog.require('goog.Disposable');
5 | goog.require('jsaction.ActionFlow');
6 | goog.require('jsaction.EventType');
7 |
8 |
9 | /**
10 | * Testing context for listening to jsaction custom events. This class should be
11 | * instantiated in a test's setUp. All listeners created in this context are
12 | * removed in CustomEvents#dispose, which should be called from the test's
13 | * tearDown.
14 | */
15 | jsaction.testing.CustomEvents = class extends goog.Disposable {
16 | constructor() {
17 | super();
18 | this.managedListeners_ = [];
19 | }
20 |
21 | /**
22 | * Adds a listener for a jsaction custom event.
23 | * @param {!Element} element The element on which to listen.
24 | * @param {string} eventType The custom event type.
25 | * @param {function(this:T, !jsaction.ActionFlow)} listener A listener
26 | * callback.
27 | * @param {!T=} opt_context The context in which to call the callback.
28 | * @param {string=} opt_flowType The ActionFlow type given to the listener.
29 | * @template T
30 | */
31 | listen(element, eventType, listener, opt_context, opt_flowType) {
32 | const jsactionListener = function(event) {
33 | if (event.detail['_type'] == eventType) {
34 | const actionFlow = new jsaction.ActionFlow(
35 | opt_flowType || jsaction.testing.CustomEvents.DEFAULT_FLOWTYPE,
36 | element, event, undefined /* startTime */, eventType);
37 | listener.call(opt_context, actionFlow);
38 | }
39 | };
40 |
41 | element.addEventListener(jsaction.EventType.CUSTOM, jsactionListener);
42 | this.managedListeners_.push(
43 | {'element': element, 'listener': jsactionListener});
44 | }
45 |
46 | /**
47 | * Removes all listeners.
48 | */
49 | disposeInternal() {
50 | for (let idx = 0; idx < this.managedListeners_.length; idx++) {
51 | const listenerInfo = this.managedListeners_[idx];
52 | listenerInfo['element'].removeEventListener(
53 | jsaction.EventType.CUSTOM, listenerInfo['listener']);
54 | }
55 | this.managedListeners_ = [];
56 | }
57 | };
58 |
59 | jsaction.testing.CustomEvents.DEFAULT_FLOWTYPE = 'jsaction.test';
60 |
--------------------------------------------------------------------------------
/customevents_test.js:
--------------------------------------------------------------------------------
1 | goog.provide('jsaction.testing.CustomEventsTest');
2 | goog.setTestOnly('jsaction.testing.CustomEventsTest');
3 |
4 | goog.require('goog.testing.jsunit');
5 | goog.require('goog.testing.recordFunction');
6 | goog.require('jsaction');
7 | goog.require('jsaction.testing.CustomEvents');
8 |
9 |
10 | var customEvents;
11 | var container;
12 | var origin;
13 |
14 |
15 | function setUpPage() {
16 | container = document.getElementById('container');
17 | origin = document.getElementById('origin');
18 | }
19 |
20 |
21 | function setUp() {
22 | customEvents = new jsaction.testing.CustomEvents();
23 | }
24 |
25 |
26 | function tearDown() {
27 | customEvents.dispose();
28 | }
29 |
30 |
31 | function testSimpleListen() {
32 | var handlerA = goog.testing.recordFunction();
33 | var handlerB = goog.testing.recordFunction();
34 | customEvents.listen(container, 'custom_a', handlerA);
35 | customEvents.listen(container, 'custom_b', handlerB);
36 |
37 | jsaction.fireCustomEvent(origin, 'custom_a');
38 | handlerA.assertCallCount(1);
39 | handlerB.assertCallCount(0);
40 | handlerA.reset();
41 |
42 | jsaction.fireCustomEvent(origin, 'custom_b');
43 | handlerA.assertCallCount(0);
44 | handlerB.assertCallCount(1);
45 | }
46 |
47 |
48 | function testMultiListen() {
49 | var handler1 = goog.testing.recordFunction();
50 | var handler2 = goog.testing.recordFunction();
51 | customEvents.listen(container, 'custom_a', handler1);
52 | customEvents.listen(container, 'custom_a', handler2);
53 |
54 | jsaction.fireCustomEvent(origin, 'custom_a');
55 | handler1.assertCallCount(1);
56 | handler2.assertCallCount(1);
57 | }
58 |
59 |
60 | function testListenWithContextAndData() {
61 | var context = {
62 | expected: 1,
63 |
64 | handler: goog.testing.recordFunction(function(actionFlow) {
65 | assertEquals(this.expected, actionFlow.event().detail.data['x']);
66 | })
67 | };
68 |
69 | customEvents.listen(container, 'custom_a', context.handler, context);
70 |
71 | jsaction.fireCustomEvent(origin, 'custom_a', {'x': 1});
72 | context.handler.assertCallCount(1);
73 | }
74 |
75 |
76 | function testDispose() {
77 | var handler = goog.testing.recordFunction();
78 | customEvents.listen(container, 'custom_a', handler);
79 |
80 | jsaction.fireCustomEvent(origin, 'custom_a');
81 | handler.assertCallCount(1);
82 | handler.reset();
83 |
84 | jsaction.fireCustomEvent(origin, 'custom_a');
85 | handler.assertCallCount(1);
86 | handler.reset();
87 |
88 | customEvents.dispose();
89 | jsaction.fireCustomEvent(origin, 'custom_a');
90 | handler.assertCallCount(0);
91 | }
92 |
--------------------------------------------------------------------------------
/customevents_test_dom.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dispatcher.js:
--------------------------------------------------------------------------------
1 | // Copyright 2005 Google Inc. All Rights Reserved.
2 |
3 |
4 | goog.provide('jsaction.Dispatcher');
5 |
6 | goog.require('goog.array');
7 | goog.require('goog.async.run');
8 | goog.require('goog.dom.TagName');
9 | goog.require('goog.functions');
10 | goog.require('goog.object');
11 | goog.require('jsaction.A11y');
12 | goog.require('jsaction.ActionFlow');
13 | goog.require('jsaction.Branch');
14 | goog.require('jsaction.Char');
15 | goog.require('jsaction.EventType');
16 | goog.require('jsaction.event');
17 | goog.require('jsaction.replayEvent');
18 | goog.requireType('jsaction.Loader');
19 |
20 |
21 |
22 | /**
23 | * An action for a namespace. It consists of two members:
24 | * accept -- whether the handler can accept the given
25 | * EventInfo immediately. If it returns false, the
26 | * dispatcher will queue the events for later replaying, which
27 | * can be triggered by calling replay().
28 | * handle -- the actual handler for the namespace.
29 | * @typedef {{accept: function(jsaction.EventInfo): boolean,
30 | * handle: function(jsaction.ActionFlow)}}
31 | */
32 | jsaction.NamespaceAction;
33 |
34 |
35 | /**
36 | * Receives a DOM event, determines the jsaction associated with the source
37 | * element of the DOM event, and invokes the handler associated with the
38 | * jsaction.
39 | *
40 | * @param {function(jsaction.EventInfo):jsaction.ActionFlow=} opt_flowFactory
41 | * A function that knows how to instantiate an ActionFlow for a particular
42 | * browser event. If not provided, a built-in one is used.
43 | * @param {function(jsaction.EventInfo):Function=} opt_getHandler A function
44 | * that knows how to get the handler for a given event info.
45 | * @param {boolean=} opt_isWiz Whether this dispatcher dispatches wiz events.
46 | * @constructor
47 | */
48 | jsaction.Dispatcher = function(opt_flowFactory, opt_getHandler, opt_isWiz) {
49 | /**
50 | * The actions that are registered for this jsaction.Dispatcher instance.
51 | *
52 | * @type {Object}
53 | * @private
54 | */
55 | this.actions_ = {};
56 |
57 | /**
58 | * A map from namespace to associated actions.
59 | * @type {!Object.}
60 | * @private
61 | */
62 | this.namespaceActions_ = {};
63 |
64 | /**
65 | * A mapping between namespaces and loader functions. We also keep a flag
66 | * indicating whether the loader was called to prevent it being called
67 | * multiple times.
68 | * @type {!Object.}
69 | * @private
70 | */
71 | this.loaders_ = {};
72 |
73 | /**
74 | * The default loader to be invoked if no loader is found for a particular
75 | * namespace.
76 | * @type {?jsaction.Loader}
77 | * @private
78 | */
79 | this.defaultLoader_ = null;
80 |
81 | /**
82 | * A list of namespaces already loaded by the default loader. This avoids
83 | * loading them once again. Using Object (with namespaces as keys) instead of
84 | * Array for O(1) search.
85 | * @type {!Object.}
86 | * @private
87 | */
88 | this.defaultLoaderNamespaces_ = {};
89 |
90 | /**
91 | * The queue of events.
92 | * @type {!Array.}
93 | * @private
94 | */
95 | this.queue_ = [];
96 |
97 | const factory = opt_flowFactory || jsaction.Dispatcher.createActionFlow_;
98 | /**
99 | * The ActionFlow factory.
100 | * @type {function(jsaction.EventInfo):jsaction.ActionFlow}
101 | * @private
102 | */
103 | this.flowFactory_ = function(eventInfo) {
104 | const actionFlow = factory(eventInfo);
105 | if (actionFlow && opt_isWiz) {
106 | actionFlow.setWiz();
107 | }
108 | return actionFlow;
109 | };
110 |
111 |
112 | /**
113 | * A function to retrieve the handler function for a given event info.
114 | * @type {function(jsaction.EventInfo):Function|undefined}
115 | * @private
116 | */
117 | this.getHandler_ = opt_getHandler;
118 |
119 | /**
120 | * A map of global event handlers, where each key is an event type.
121 | * @private {!Object.>}
122 | */
123 | this.globalHandlers_ = {};
124 |
125 | /**
126 | * @private {?function(
127 | * !Array., !jsaction.Dispatcher):void}
128 | */
129 | this.eventReplayer_ = null;
130 | };
131 |
132 |
133 | /**
134 | * Receives an event or the event queue from the EventContract. The event
135 | * queue is copied and it attempts to replay.
136 | * If event info is passed in it looks for an action handler that can handle
137 | * the given event. If there is no handler registered queues the event and
138 | * checks if a loader is registered for the given namespace. If so, calls it.
139 | *
140 | * Alternatively, if in global dispatch mode, calls all registered global
141 | * handlers for the appropriate event type.
142 | *
143 | * The three functionalities of this call are deliberately not split into three
144 | * methods (and then declared as an abstract interface), because the interface
145 | * is used by EventContract, which lives in a different jsbinary. Therefore the
146 | * interface between the three is defined entirely in terms that are invariant
147 | * under jscompiler processing (Function and Array, as opposed to a custom type
148 | * with method names).
149 | *
150 | * @param {(!jsaction.EventInfo|!Array)} eventInfo
151 | * The info for the event that triggered this call or the queue of events
152 | * from EventContract.
153 | * @param {boolean=} isGlobalDispatch If true, dispatches a global event
154 | * instead of a regular jsaction handler.
155 | * @return {!Event|undefined} Returns an event for the event contract to handle
156 | * again IFF we tried to resolve an a11y event that can't be casted to a
157 | * click.
158 | */
159 | jsaction.Dispatcher.prototype.dispatch = function(eventInfo, isGlobalDispatch) {
160 | if (goog.isArray(eventInfo)) {
161 | // We received the queued events from EventContract. Copy them and try to
162 | // replay.
163 | this.queue_ = this.cloneEventInfoQueue(eventInfo);
164 | this.replayQueuedEvents_();
165 | return;
166 | }
167 |
168 | const resolvedA11yEvent =
169 | this.maybeResolveA11yEvent(eventInfo, isGlobalDispatch);
170 | if (resolvedA11yEvent['needsRetrigger']) {
171 | return resolvedA11yEvent['event'];
172 | }
173 | eventInfo = resolvedA11yEvent;
174 |
175 | if (isGlobalDispatch) {
176 | // Skip everything related to jsaction handlers, and execute the global
177 | // handlers.
178 | const ev = eventInfo['event'];
179 | const eventTypeHandlers = this.globalHandlers_[eventInfo['eventType']];
180 | let shouldPreventDefault = false;
181 | if (eventTypeHandlers) {
182 | for (let idx = 0, handler; handler = eventTypeHandlers[idx++];) {
183 | if (handler(ev) === false) {
184 | shouldPreventDefault = true;
185 | }
186 | }
187 | }
188 | if (shouldPreventDefault) {
189 | jsaction.event.preventDefault(ev);
190 | }
191 | return;
192 | }
193 |
194 | const action = eventInfo['action'];
195 | const namespace = jsaction.Dispatcher.getNamespace_(action);
196 | const namespaceAction = this.namespaceActions_[namespace];
197 |
198 | let handler;
199 | if (this.getHandler_) {
200 | handler = this.getHandler_(eventInfo);
201 | } else if (!namespaceAction) {
202 | handler = this.actions_[action];
203 | } else if (namespaceAction.accept(eventInfo)) {
204 | handler = namespaceAction.handle;
205 | }
206 |
207 | if (handler) {
208 | const stats = this.flowFactory_(
209 | /** @type {!jsaction.EventInfo} */ (eventInfo));
210 | handler(stats);
211 | stats.done(jsaction.Branch.MAIN);
212 | return;
213 | }
214 |
215 | // No handler was found. Potentially make a copy of the event to extend its
216 | // life and queue it.
217 | const eventCopy = jsaction.event.maybeCopyEvent(eventInfo['event']);
218 | eventInfo['event'] = eventCopy;
219 | this.queue_.push(eventInfo);
220 |
221 | if (!namespaceAction) {
222 | // If there is no handler, check if there is a loader available.
223 | // If there already is a handler for the namespace, but it is not
224 | // yet ready to accept the event, then the namespace handler
225 | // might load handlers on its own, and will call replay() later.
226 | this.maybeInvokeLoader_(namespace, eventInfo);
227 | }
228 | };
229 |
230 |
231 | /**
232 | * Makes a shallow copy of the EventInfo queue, where any MAYBE_CLICK_EVENT_TYPE
233 | * typed events get their type converted to CLICK or KEYDOWN.
234 | * Because clients of jsaction must provide their own implementation of how to
235 | * replay queued events, this removes the need for those clients to know how to
236 | * handle MAYBE_CLICK_EVENT_TYPE events.
237 | *
238 | * @param {!Array} eventInfoQueue
239 | * @return {!Array}
240 | */
241 | jsaction.Dispatcher.prototype.cloneEventInfoQueue = function(eventInfoQueue) {
242 | const resolvedEventInfoQueue = [];
243 | for (let i = 0; i < eventInfoQueue.length; i++) {
244 | const resolvedEventInfo = this.maybeResolveA11yEvent(eventInfoQueue[i]);
245 | if (resolvedEventInfo['needsRetrigger']) {
246 | // Normally the event contract will check for the needsRetrigger value
247 | // after a dispatch, but in the case of replaying a queue, the replay
248 | // function decides how to handle each eventInfo without going through the
249 | // event contract. Since these events need to have the appropriate action
250 | // for them found, we will replay them so that they can be caught and
251 | // handled by the contract.
252 | jsaction.replayEvent(resolvedEventInfo);
253 | } else {
254 | resolvedEventInfoQueue.push(resolvedEventInfo);
255 | }
256 | }
257 |
258 | return resolvedEventInfoQueue;
259 | };
260 |
261 | /**
262 | * If a 'MAYBE_CLICK_EVENT_TYPE' event was dispatched, updates the eventType to
263 | * either click or keydown based on whether the keydown action can be treated as
264 | * a click. For MAYBE_CLICK_EVENT_TYPE events that are just keydowns, we set
265 | * flags on the event object so that the event contract does't try to dispatch
266 | * it as a MAYBE_CLICK_EVENT_TYPE again.
267 | *
268 | * @param {!jsaction.EventInfo} eventInfo
269 | * @param {boolean=} isGlobalDispatch Whether the eventInfo is meant to be
270 | * dispatched to the global handlers.
271 | * @return {!jsaction.EventInfo} Returns a jsaction.EventInfo object with the
272 | * MAYBE_CLICK_EVENT_TYPE converted to CLICK or KEYDOWN.
273 | */
274 | jsaction.Dispatcher.prototype.maybeResolveA11yEvent = function(
275 | eventInfo, isGlobalDispatch = false) {
276 | if (eventInfo['eventType'] !== jsaction.A11y.MAYBE_CLICK_EVENT_TYPE) {
277 | return eventInfo;
278 | }
279 |
280 | const /** !jsaction.EventInfo */ eventInfoCopy =
281 | /** @type {!jsaction.EventInfo} */ (goog.object.clone(eventInfo));
282 | const event = eventInfoCopy['event'];
283 |
284 | if (this.isA11yClickEvent_(eventInfo, isGlobalDispatch)) {
285 | if (this.shouldPreventDefault_(eventInfoCopy)) {
286 | jsaction.event.preventDefault(event);
287 | }
288 | // If the keydown event can be treated as a click, we change the eventType
289 | // to 'click' so that the dispatcher can retrieve the right handler for it.
290 | // Even though EventInfo['action'] corresponds to the click action, the
291 | // global handler and any custom 'getHandler' implementations may rely on
292 | // the eventType instead.
293 | eventInfoCopy['eventType'] = jsaction.EventType.CLICK;
294 | } else {
295 | // Otherwise, if the keydown can't be treated as a click, we need to
296 | // retrigger it because now we need to look for 'keydown' actions instead.
297 | eventInfoCopy['eventType'] = jsaction.EventType.KEYDOWN;
298 | if (!isGlobalDispatch) {
299 | const eventCopy = jsaction.event.maybeCopyEvent(event);
300 | // This prevents the event contract from setting the
301 | // jsaction.A11y.MAYBE_CLICK_EVENT_TYPE type for Keydown events.
302 | eventCopy[jsaction.A11y.SKIP_A11Y_CHECK] = true;
303 | // Since globally dispatched events will get handled by the dispatcher,
304 | // don't have the event contract dispatch it again.
305 | eventCopy[jsaction.A11y.SKIP_GLOBAL_DISPATCH] = true;
306 | eventInfoCopy['event'] = eventCopy;
307 | // Cancels the dispatch early and tells the dispatcher to send this event
308 | // back to the event contract.
309 | eventInfoCopy['needsRetrigger'] = true;
310 | }
311 | }
312 | return eventInfoCopy;
313 | };
314 |
315 | /**
316 | * Returns true if the given key event can be treated as a 'click'.
317 | *
318 | * @param {!jsaction.EventInfo} eventInfo
319 | * @param {boolean=} isGlobalDispatch Whether the eventInfo is meant to be
320 | * dispatched to the global handlers.
321 | * @return {boolean}
322 | * @private
323 | */
324 | jsaction.Dispatcher.prototype.isA11yClickEvent_ = function(
325 | eventInfo, isGlobalDispatch) {
326 | return (isGlobalDispatch || eventInfo['actionElement']) &&
327 | jsaction.event.isActionKeyEvent(eventInfo['event']);
328 | };
329 |
330 | /**
331 | * Returns true if the default action for this event should be prevented
332 | * before the event handler is envoked.
333 | *
334 | * @param {!jsaction.EventInfo} eventInfo
335 | * @return {boolean}
336 | * @private
337 | */
338 | jsaction.Dispatcher.prototype.shouldPreventDefault_ = function(eventInfo) {
339 | // For parity with no-a11y-support behavior.
340 | if (!eventInfo['actionElement']) {
341 | return false;
342 | }
343 | const event = eventInfo['event'];
344 | // Prevent scrolling if the Space key was pressed
345 | if (jsaction.event.isSpaceKeyEvent(event)) {
346 | return true;
347 | }
348 | // or prevent the browser's default action for native HTML controls.
349 | if (jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(event)) {
350 | return true;
351 | }
352 | // Prevent browser from following node links if a jsaction is present
353 | // and we are dispatching the action now. Note that the targetElement may be a
354 | // child of an anchor that has a jsaction attached. For that reason, we need
355 | // to check the actionElement rather than the targetElement.
356 | if (eventInfo['actionElement'].tagName == goog.dom.TagName.A) {
357 | return true;
358 | }
359 | return false;
360 | };
361 |
362 |
363 |
364 | /**
365 | * Registers a loader function to be called in case a jsaction is
366 | * encountered for which there is no handler registered. The loader is
367 | * expected to register the jsaction handlers for the given namespace.
368 | *
369 | * @param {string} actionNamespace The action namespace.
370 | * @param {jsaction.Loader} loaderFn The loader that will install the action
371 | * handlers for this namespace. It takes the dispatcher and the namespace as
372 | * parameters.
373 | */
374 | jsaction.Dispatcher.prototype.registerLoader = function(
375 | actionNamespace, loaderFn) {
376 | this.loaders_[actionNamespace] = {loader: loaderFn, called: false};
377 | };
378 |
379 |
380 | /**
381 | * Registers the default loader function to be called if no specific loader
382 | * exists for a given namespace.
383 | *
384 | * @param {jsaction.Loader} loaderFn The loader that will install the action
385 | * handlers for this namespace. It takes the dispatcher and the namespace
386 | * as parameters.
387 | */
388 | jsaction.Dispatcher.prototype.registerDefaultLoader = function(loaderFn) {
389 | this.defaultLoader_ = loaderFn;
390 | };
391 |
392 |
393 | /**
394 | * Registers a handler for a whole namespace. The dispatcher will
395 | * dispatch all jsaction for the given namespace to the handler.
396 | *
397 | * Namespace handlers has higher precedence than other handlers/loader.
398 | *
399 | * @param {string} namespace The namespace to register handler on.
400 | * @param {function(jsaction.ActionFlow)} handler The handler function.
401 | * @param {(function(jsaction.EventInfo):boolean)=} opt_accept
402 | * A function that, given the EventInfo, can determine whether
403 | * the event should be immediately handled or be queued. Defaults
404 | * to always returning true.
405 | */
406 | jsaction.Dispatcher.prototype.registerNamespaceHandler = function(
407 | namespace, handler, opt_accept) {
408 | this.namespaceActions_[namespace] = {
409 | accept: opt_accept || goog.functions.TRUE,
410 | handle: handler
411 | };
412 | };
413 |
414 |
415 | /**
416 | * Invokes the loader for the namespace if there is one and it wasn't called
417 | * already. The dispatcher is passed as a parameter to the loader. If no
418 | * loader is found for the namespace, invoke the default loader.
419 | *
420 | * @param {string} namespace The namespace.
421 | * @param {jsaction.EventInfo} eventInfo The event info.
422 | * @private
423 | */
424 | jsaction.Dispatcher.prototype.maybeInvokeLoader_ = function(
425 | namespace, eventInfo) {
426 | const loaderInfo = this.loaders_[namespace];
427 | if (!loaderInfo) {
428 | if (this.defaultLoader_ && !(namespace in this.defaultLoaderNamespaces_)) {
429 | this.defaultLoaderNamespaces_[namespace] = true;
430 | this.defaultLoader_(this, namespace, eventInfo);
431 | }
432 | } else if (!loaderInfo.called) {
433 | loaderInfo.loader(this, namespace, eventInfo);
434 | loaderInfo.called = true;
435 | }
436 | };
437 |
438 |
439 | /**
440 | * Extracts and returns the namespace from a fully qualified jsaction
441 | * of the form "namespace.actionname".
442 | * @param {string} action The action.
443 | * @return {string} The namespace.
444 | * @private
445 | */
446 | jsaction.Dispatcher.getNamespace_ = function(action) {
447 | return action.split('.')[0];
448 | };
449 |
450 |
451 | /**
452 | * Creates a jsaction.ActionFlow to be passed to an action handler.
453 | * @param {jsaction.EventInfo} eventInfo The event info.
454 | * @return {jsaction.ActionFlow} The newly created ActionFlow.
455 | * @private
456 | */
457 | jsaction.Dispatcher.createActionFlow_ = function(eventInfo) {
458 | return new jsaction.ActionFlow(
459 | eventInfo['action'], eventInfo['actionElement'], eventInfo['event'],
460 | eventInfo['timeStamp'], eventInfo['eventType'],
461 | eventInfo['targetElement']);
462 | };
463 |
464 |
465 | /**
466 | * Registers multiple methods all bound to the same object
467 | * instance. This is a common case: an application module binds
468 | * multiple of its methods under public names to the event contract of
469 | * the application. So we provide a shortcut for it.
470 | * Attempts to replay the queued events after registering the handlers.
471 | *
472 | * @param {string} namespace The namespace of the jsaction name.
473 | * NOTE(user): This is not optional in order to encourage uniform
474 | * naming for all methods registered by a module.
475 | *
476 | * @param {Object} instance The object to bind the methods to. If this
477 | * is null, then the functions are not bound, but directly added
478 | * under the public names.
479 | *
480 | * @param {!Object.} methods
481 | * A map from public name to functions that will be bound
482 | * to instance and registered as action under the public
483 | * name. I.e. the property names are the public names. The
484 | * property values are the methods of instance.
485 | */
486 | jsaction.Dispatcher.prototype.registerHandlers = function(
487 | namespace, instance, methods) {
488 | goog.object.forEach(methods, goog.bind(function(method, name) {
489 | const handler = instance ? goog.bind(method, instance) : method;
490 | // Include a '.' separator between namespace name and action name.
491 | // In the case that no namespace name is provided, the jsaction name
492 | // consists of the action name only (no period).
493 | if (namespace) {
494 | const fullName =
495 | namespace + jsaction.Char.NAMESPACE_ACTION_SEPARATOR + name;
496 | this.actions_[fullName] = handler;
497 | } else {
498 | this.actions_[name] = handler;
499 | }
500 | }, this));
501 |
502 | this.replayQueuedEvents_();
503 | };
504 |
505 |
506 | /**
507 | * Unregisters an action. Provided as an easy way to reverse the effects of
508 | * registerHandlers.
509 | * @param {string} namespace The namespace of the jsaction name.
510 | * @param {string} name The action name to unbind.
511 | */
512 | jsaction.Dispatcher.prototype.unregisterHandler = function(namespace, name) {
513 | const fullName = namespace ?
514 | namespace + jsaction.Char.NAMESPACE_ACTION_SEPARATOR + name :
515 | name;
516 | delete this.actions_[fullName];
517 | };
518 |
519 |
520 | /**
521 | * Registers a global event handler.
522 | * @param {string} eventType
523 | * @param {function(!Event):(boolean|undefined)} handler
524 | */
525 | jsaction.Dispatcher.prototype.registerGlobalHandler = function(
526 | eventType, handler) {
527 | this.globalHandlers_[eventType] = this.globalHandlers_[eventType] || [];
528 | this.globalHandlers_[eventType].push(handler);
529 | };
530 |
531 |
532 | /**
533 | * Unregisters a global event handler.
534 | * @param {string} eventType
535 | * @param {function(!Event):(boolean|undefined)} handler
536 | */
537 | jsaction.Dispatcher.prototype.unregisterGlobalHandler = function(
538 | eventType, handler) {
539 | if (this.globalHandlers_[eventType]) {
540 | goog.array.remove(this.globalHandlers_[eventType], handler);
541 | }
542 | };
543 |
544 |
545 | /**
546 | * Checks whether there is an action registered under the given
547 | * name. This returns true if there is a namespace handler, even
548 | * if it can not yet handle the event.
549 | *
550 | * TODO(chrishenry): Remove this when canDispatch is used everywhere.
551 | *
552 | * @param {string} name Action name.
553 | * @return {boolean} Whether the name is registered.
554 | * @see #canDispatch
555 | */
556 | jsaction.Dispatcher.prototype.hasAction = function(name) {
557 | return this.actions_.hasOwnProperty(name) ||
558 | this.namespaceActions_.hasOwnProperty(
559 | jsaction.Dispatcher.getNamespace_(name));
560 | };
561 |
562 |
563 | /**
564 | * Whether this dispatcher can dispatch the event. This can be used by
565 | * event replayer to check whether the dispatcher can replay an event.
566 | * @param {jsaction.EventInfo} eventInfo
567 | * @return {boolean}
568 | */
569 | jsaction.Dispatcher.prototype.canDispatch = function(eventInfo) {
570 | const name = eventInfo['action'];
571 | if (this.actions_.hasOwnProperty(name)) {
572 | return true;
573 | }
574 | const ns = jsaction.Dispatcher.getNamespace_(name);
575 | if (this.namespaceActions_.hasOwnProperty(ns)) {
576 | return this.namespaceActions_[ns].accept(eventInfo);
577 | }
578 | return false;
579 | };
580 |
581 |
582 | /**
583 | * Replays queued events, if any. The replaying will happen in its own
584 | * stack once the current flow cedes control. This is done to mimic
585 | * browser event handling.
586 | */
587 | jsaction.Dispatcher.prototype.replay = function() {
588 | this.replayQueuedEvents_();
589 | };
590 |
591 |
592 | /**
593 | * Replays queued events, if any. The replaying will happen in its own
594 | * stack once the current flow cedes control. As opposed to the replay()
595 | * method, the replay happens immediately.
596 | */
597 | jsaction.Dispatcher.prototype.replayNow = function() {
598 | if (!this.eventReplayer_ || goog.array.isEmpty(this.queue_)) {
599 | return;
600 | }
601 | this.eventReplayer_(this.queue_, this);
602 | };
603 |
604 |
605 | /**
606 | * Replays queued events. The replaying will happen in its own stack once the
607 | * current flow cedes control. This is done to mimic browser event handling.
608 | * @private
609 | */
610 | jsaction.Dispatcher.prototype.replayQueuedEvents_ = function() {
611 | if (!this.eventReplayer_ || goog.array.isEmpty(this.queue_)) {
612 | return;
613 | }
614 | goog.async.run(function() {
615 | this.eventReplayer_(this.queue_, this);
616 | }, this);
617 | };
618 |
619 |
620 | /**
621 | * Sets the event replayer, enabling queued events to be replayed when actions
622 | * are bound. After setting the event replayer, tries to replay queued events.
623 | * The event replayer takes as parameters the queue of events and the dispatcher
624 | * (used to check whether actions have handlers registered and can be replayed).
625 | * The event replayer is also responsible for dequeuing events.
626 | *
627 | * Example: An event replayer that replays only the last event.
628 | *
629 | * const dispatcher = new Dispatcher;
630 | * // ...
631 | * dispatcher.setEventReplayer(function(queue, dispatcher) {
632 | * const lastEventInfo = goog.array.peek(queue);
633 | * if (dispatcher.canDispatch(lastEventInfo.action) {
634 | * jsaction.replay.replayEvent(lastEventInfo);
635 | * goog.array.clear(queue);
636 | * }
637 | * });
638 | *
639 | * @param {function(!Array., !jsaction.Dispatcher):void}
640 | * eventReplayer It allows elements to be replayed and dequeuing.
641 | */
642 | jsaction.Dispatcher.prototype.setEventReplayer = function(eventReplayer) {
643 | this.eventReplayer_ = eventReplayer;
644 | this.replayQueuedEvents_();
645 | };
646 |
--------------------------------------------------------------------------------
/dispatcher_auto.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Instantiates a jsaction.Dispatcher and connects it with an
3 | * instance of jsaction.EventContract that it receives from the main HTML page.
4 | */
5 |
6 | goog.provide('jsaction.dispatcherAuto');
7 |
8 | goog.require('jsaction.Dispatcher');
9 |
10 |
11 | /**
12 | * Registers a jsaction handler.
13 | * @param {string} action
14 | * @param {function(this:Element,Event)} handler
15 | * @param {Object=} opt_instance
16 | * @this jsaction.Dispatcher
17 | */
18 | function register(action, handler, opt_instance) {
19 | var separatorIndex = action.indexOf('.');
20 | var namespace = action.substr(0, separatorIndex);
21 | var actionName = action.substr(separatorIndex + 1);
22 | var handlerMap = {};
23 | handlerMap[actionName] = function(actionFlow) {
24 | handler.call(actionFlow.node(), actionFlow.event());
25 | };
26 | this.registerHandlers(namespace, opt_instance || null, handlerMap);
27 | }
28 |
29 |
30 | /**
31 | * Unregisters a jsaction handler.
32 | * @param {string} action
33 | * @this jsaction.Dispatcher
34 | */
35 | function unregister(action) {
36 | var separatorIndex = action.indexOf('.');
37 | var namespace = action.substr(0, separatorIndex);
38 | var actionName = action.substr(separatorIndex + 1);
39 | this.unregisterHandler(namespace, actionName);
40 | }
41 |
42 |
43 | /**
44 | * Creates a dispatcher and exposes a public API.
45 | * @param {!Object} global
46 | */
47 | function main(global) {
48 | // If we can't find the exported jsaction namespace, it means we don't have an
49 | // available contract.
50 | if (!global['jsaction']) {
51 | return;
52 | }
53 | // Binds a dispatcher to the contract.
54 | var dispatcher = new jsaction.Dispatcher();
55 | global['jsaction']['__dispatchTo'](
56 | goog.bind(dispatcher.dispatch, dispatcher));
57 |
58 | // Exposes JsAction's public API.
59 | goog.exportSymbol('jsaction.register', goog.bind(register, dispatcher));
60 | goog.exportSymbol('jsaction.unregister', goog.bind(unregister, dispatcher));
61 | }
62 |
63 | // Bootstraps the dispatcher.
64 | main(goog.global);
65 |
--------------------------------------------------------------------------------
/dispatcher_example.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All Rights Reserved.
2 |
3 | /**
4 | *
5 | * @fileoverview The entry point for a JS binary that instantiates
6 | * jsaction.Dispatcher and connects it with an instance of
7 | * jsaction.EventContract that it receives from the main HTML
8 | * page. This is meant to be part of an externally loaded JS binary.
9 | *
10 | * Cf. eventcontract_example.js, the inlined counterpart.
11 | *
12 | * This file serves as model for how Dispatcher and EventContract
13 | * cooperate, and to check that the code jscompiles properly.
14 | */
15 |
16 | goog.require('jsaction.ActionFlow');
17 | goog.require('jsaction.Dispatcher');
18 | goog.require('jsaction.replayEvent');
19 |
20 |
21 |
22 | /**
23 | * This function is executed when the external js finishes loading. It
24 | * must be the last thing in the js. It calls a well known function on
25 | * window which was placed there by the event contract and passes its
26 | * own event callback there, where it's registered with event
27 | * contract.
28 | */
29 | (function main() {
30 | var d = new jsaction.Dispatcher;
31 | d.registerHandlers('foo', null, {'bar': function() {}});
32 |
33 | var stats = new jsaction.ActionFlow('test_flow');
34 | stats.tick('t0');
35 |
36 | jsaction.replayEvent(/** @type {!jsaction.EventInfo} */ ({
37 | 'action': 'foo.bar',
38 | 'event': /** @type {!Event} */ ({}),
39 | 'eventType': 'click',
40 | 'targetElement': /** @type {!Element} */ ({}),
41 | 'actionElement': /** @type {!Element} */ ({}),
42 | 'timeStamp': 1234
43 | }));
44 |
45 | // See eventcontract_main.js.
46 | window['dispatcherOnLoad'](goog.bind(d.dispatch, d));
47 | })();
48 |
--------------------------------------------------------------------------------
/dispatcher_export.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview File that exports symbols from the dispatcher to be used
3 | * in standalone binaries that drop the dispatcher script into their page.
4 | */
5 |
6 | goog.provide('jsaction.dispatcherExport');
7 |
8 | goog.require('jsaction.ActionFlow');
9 | goog.require('jsaction.Dispatcher');
10 |
11 |
12 | goog.exportSymbol('jsaction.ActionFlow', jsaction.ActionFlow);
13 | goog.exportSymbol(
14 | 'jsaction.ActionFlow.prototype.event', jsaction.ActionFlow.prototype.event);
15 | goog.exportSymbol(
16 | 'jsaction.ActionFlow.prototype.eventType',
17 | jsaction.ActionFlow.prototype.eventType);
18 | goog.exportSymbol(
19 | 'jsaction.ActionFlow.prototype.node', jsaction.ActionFlow.prototype.node);
20 |
21 | goog.exportSymbol('jsaction.Dispatcher', jsaction.Dispatcher);
22 | goog.exportSymbol(
23 | 'jsaction.Dispatcher.prototype.dispatch',
24 | jsaction.Dispatcher.prototype.dispatch);
25 | goog.exportSymbol(
26 | 'jsaction.Dispatcher.prototype.registerHandlers',
27 | jsaction.Dispatcher.prototype.registerHandlers);
28 | goog.exportSymbol(
29 | 'jsaction.Dispatcher.prototype.setEventReplayer',
30 | jsaction.Dispatcher.prototype.setEventReplayer);
31 |
--------------------------------------------------------------------------------
/dispatcher_test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2007 Google Inc. All rights reserved.
2 |
3 | /**
4 | */
5 |
6 | /** @suppress {extraProvide} */
7 | goog.provide('jsaction.DispatcherTest');
8 | goog.setTestOnly('jsaction.DispatcherTest');
9 |
10 | goog.require('goog.testing.MockClock');
11 | goog.require('goog.testing.MockControl');
12 | goog.require('goog.testing.jsunit');
13 | goog.require('goog.testing.mockmatchers');
14 | goog.require('goog.testing.recordFunction');
15 | goog.require('jsaction.ActionFlow');
16 | goog.require('jsaction.Dispatcher');
17 | /** @suppress {extraRequire} */
18 | goog.require('jsaction.replayEvent');
19 |
20 |
21 | var mockClock_;
22 | var mockControl_;
23 | var isObject_ = goog.testing.mockmatchers.isObject;
24 | var isArray_ = goog.testing.mockmatchers.isArray;
25 |
26 |
27 | function setUp() {
28 | mockControl_ = new goog.testing.MockControl;
29 | mockClock_ = new goog.testing.MockClock;
30 | mockClock_.install();
31 | }
32 |
33 |
34 | function tearDown() {
35 | mockControl_.$tearDown();
36 | mockClock_.uninstall();
37 | }
38 |
39 |
40 | function testDispatcherHandleAction_HandlerBound() {
41 | var actionHandler = mockControl_.createFunctionMock();
42 | var mockActionElement = document.createElement('div');
43 | var mockEvent = jsaction.createEvent({type: 'click'});
44 |
45 | var actionFlow = null;
46 | actionHandler(isObject_).$does(function(flow) {
47 | actionFlow = flow;
48 | actionFlow.branch('fakebranch');
49 | });
50 |
51 | mockControl_.$replayAll();
52 |
53 | var d = new jsaction.Dispatcher;
54 | var actions = {'bar': actionHandler};
55 | d.registerHandlers('foo', null, actions);
56 |
57 | d.dispatch({
58 | action: 'foo.bar',
59 | actionElement: mockActionElement,
60 | event: mockEvent
61 | });
62 | assertNotNull(actionFlow);
63 | assertEquals('foo_bar', actionFlow.getType());
64 | assertEquals(mockEvent.type, actionFlow.event().type);
65 | assertEquals(mockActionElement, actionFlow.node());
66 |
67 | mockControl_.$verifyAll();
68 | }
69 |
70 |
71 | function testDispatcherHandleAction_NoHandlerBound_CallLoader() {
72 | var loader = mockControl_.createFunctionMock();
73 | var mockEvent = jsaction.createEvent({type: 'click'});
74 |
75 | // The loader should get called only once.
76 | loader(isObject_, 'foo', isObject_);
77 |
78 | mockControl_.$replayAll();
79 |
80 | var d = new jsaction.Dispatcher;
81 | d.registerLoader('foo', loader);
82 |
83 | d.dispatch({action: 'foo.bar', event: mockEvent});
84 | d.dispatch({action: 'foo.bar', event: mockEvent});
85 |
86 | mockControl_.$verifyAll();
87 | }
88 |
89 |
90 | function testRegisterHandlers() {
91 | var d = new jsaction.Dispatcher;
92 |
93 | // An object to whose methods we bind actions. The properties are
94 | // methods (hence unquoted).
95 | var o = {
96 | foo: function() {},
97 | bar: function() {},
98 | baz: function() {}
99 | };
100 |
101 | // The config which action to map to which method of o. The
102 | // properties are names of actions used in the value of the jsaction
103 | // HTML attribute (hence quoted). The difference would be
104 | // significant in jscompiled code.
105 | var m = {
106 | 'foo': o.foo,
107 | 'bar': o.bar
108 | };
109 |
110 | d.registerHandlers('', o, m);
111 | assertTrue(d.hasAction('foo'));
112 | assertTrue(d.hasAction('bar'));
113 | assertFalse(d.hasAction('baz'));
114 |
115 |
116 | d.registerHandlers('x', o, m);
117 | assertTrue(d.hasAction('x.foo'));
118 | assertTrue(d.hasAction('x.bar'));
119 | assertFalse(d.hasAction('x.baz'));
120 | }
121 |
122 |
123 | function testUnregisterHandlers() {
124 | var d = new jsaction.Dispatcher;
125 | var handler1Called = false;
126 | var handler1 = function() {
127 | handler1Called = true;
128 | };
129 | var handler2Called = false;
130 | var handler2 = function() {
131 | handler2Called = true;
132 | };
133 |
134 | d.registerHandlers('prefix', null, {'clickaction': handler1});
135 | d.registerHandlers('', null, {'fooaction': handler2});
136 | assertTrue(d.hasAction('prefix.clickaction'));
137 | assertTrue(d.hasAction('fooaction'));
138 |
139 | d.unregisterHandler('prefix', 'clickaction');
140 | assertFalse(d.hasAction('prefix.clickaction'));
141 | assertFalse(handler1Called);
142 |
143 | d.unregisterHandler('', 'fooaction');
144 | assertFalse(d.hasAction('fooaction'));
145 | assertFalse(handler2Called);
146 | }
147 |
148 |
149 | function testEventAreReplayedWhenQueuePassedIn() {
150 | var d = new jsaction.Dispatcher;
151 | var mockEventReplayer = mockControl_.createFunctionMock();
152 | var mockEvent = jsaction.createEvent({type: 'click'});
153 | var mockQueue = [{action: 'foo.bar', event: mockEvent}];
154 | var replayed = false;
155 |
156 | mockEventReplayer(isArray_, d).$does(function() {
157 | replayed = true;
158 | });
159 |
160 | mockControl_.$replayAll();
161 |
162 | var actions = {'bar': function() {}};
163 | d.registerHandlers('foo', null, actions);
164 | d.setEventReplayer(mockEventReplayer);
165 |
166 | assertFalse(replayed);
167 |
168 | d.dispatch(mockQueue);
169 | mockClock_.tick(0);
170 |
171 | assertTrue(replayed);
172 |
173 | mockControl_.$verifyAll();
174 | }
175 |
176 |
177 | function testEventAreReplayedWhenHandlersAreRegistered() {
178 | var d = new jsaction.Dispatcher;
179 | var mockEventReplayer = mockControl_.createFunctionMock();
180 | var mockEvent = jsaction.createEvent({type: 'click'});
181 | var mockQueue = [{action: 'foo.bar', event: mockEvent}];
182 | var replayed = false;
183 |
184 | mockEventReplayer(isArray_, d).$does(function() {
185 | replayed = true;
186 | });
187 |
188 | mockControl_.$replayAll();
189 |
190 | d.setEventReplayer(mockEventReplayer);
191 | d.dispatch({action: 'foo.bar', event: mockEvent});
192 |
193 | assertFalse(replayed);
194 |
195 | var actions = {'bar': function() {}};
196 | d.registerHandlers('foo', null, actions);
197 | mockClock_.tick(0);
198 |
199 | assertTrue(replayed);
200 |
201 | mockControl_.$verifyAll();
202 | }
203 |
204 |
205 | function testEventsAreReplayedWhenReplayerIsRegistered() {
206 | var d = new jsaction.Dispatcher;
207 | var mockEventReplayer = mockControl_.createFunctionMock();
208 | var mockEvent = jsaction.createEvent({type: 'click'});
209 | var mockQueue = [{action: 'foo.bar', event: mockEvent}];
210 | var replayed = false;
211 |
212 | mockEventReplayer(isArray_, d).$does(function() {
213 | replayed = true;
214 | });
215 |
216 | mockControl_.$replayAll();
217 |
218 | d.dispatch({action: 'foo.bar', event: mockEvent});
219 | var actions = {'bar': function() {}};
220 | d.registerHandlers('foo', null, actions);
221 |
222 | assertFalse(replayed);
223 |
224 | d.setEventReplayer(mockEventReplayer);
225 | mockClock_.tick(0);
226 |
227 | assertTrue(replayed);
228 |
229 | mockControl_.$verifyAll();
230 | }
231 |
232 |
233 | function testAlternateFlowFactory() {
234 | var mockEvent = jsaction.createEvent({type: 'click'});
235 | var eventInfo = {action: 'foo.bar', event: mockEvent};
236 | var mockFlowFactory = mockControl_.createFunctionMock();
237 | var d = new jsaction.Dispatcher(mockFlowFactory);
238 | var actionFlow = new jsaction.ActionFlow('foo.bar');
239 | var flowFactoryInvoked = false;
240 | mockFlowFactory(eventInfo).$does(function() {
241 | flowFactoryInvoked = true;
242 | return actionFlow;
243 | });
244 |
245 | var handled = false;
246 | var mockHandler = mockControl_.createFunctionMock();
247 | mockHandler(actionFlow).$does(function() {
248 | handled = true;
249 | });
250 |
251 | mockControl_.$replayAll();
252 |
253 | var actions = {'bar': mockHandler};
254 | d.registerHandlers('foo', null, actions);
255 |
256 | assertFalse(handled);
257 | assertFalse(flowFactoryInvoked);
258 | d.dispatch(eventInfo);
259 | assertTrue(handled);
260 | assertTrue(flowFactoryInvoked);
261 |
262 | mockControl_.$verifyAll();
263 | }
264 |
265 |
266 | function testRegisterLoader() {
267 | var d = new jsaction.Dispatcher;
268 | var mockLoader = function() {};
269 | d.registerLoader('foo', mockLoader);
270 |
271 | assertObjectEquals({'foo': {loader: mockLoader, called: false}}, d.loaders_);
272 | }
273 |
274 |
275 | function testRegisterDefaultLoader() {
276 | var d = new jsaction.Dispatcher;
277 | var mockEvent = jsaction.createEvent({type: 'click'});
278 | var mockDefaultLoaderCalled = false;
279 | var mockDefaultLoader = function() {
280 | mockDefaultLoaderCalled = true;
281 | };
282 | d.registerDefaultLoader(mockDefaultLoader);
283 | d.dispatch({action: 'foo.bar', event: mockEvent});
284 |
285 | assertTrue(mockDefaultLoaderCalled);
286 | }
287 |
288 |
289 | function testMaybeInvokeLoaderWithoutLoaders() {
290 | var d = new jsaction.Dispatcher;
291 | var mockEvent = jsaction.createEvent({type: 'click'});
292 | var mockDefaultLoaderCalled = false;
293 | var mockDefaultLoader = function() {
294 | mockDefaultLoaderCalled = true;
295 | };
296 | d.dispatch({action: 'foo.bar', event: mockEvent});
297 |
298 | assertFalse(mockDefaultLoaderCalled);
299 | }
300 |
301 |
302 | function testMaybeInvokeLoaderWithoutDefault() {
303 | var d = new jsaction.Dispatcher;
304 | var mockEvent = jsaction.createEvent({type: 'click'});
305 | var loaderCalled = false;
306 | var loaderDispatcher;
307 | var loaderNamespace;
308 | var loader = function(dispatcher, namespace) {
309 | loaderCalled = true;
310 | loaderDispatcher = dispatcher;
311 | loaderNamespace = namespace;
312 | };
313 | d.registerLoader('foo', loader);
314 | d.dispatch({action: 'foo.bar', event: mockEvent});
315 |
316 | assertTrue(loaderCalled);
317 | assertEquals(d, loaderDispatcher);
318 | assertEquals('foo', loaderNamespace);
319 | }
320 |
321 |
322 | function testMaybeInvokeLoaderWithoutDefaultButUnmatchedNamespace() {
323 | var d = new jsaction.Dispatcher;
324 | var mockEvent = jsaction.createEvent({type: 'click'});
325 | var loaderCalled = false;
326 | var loaderDispatcher;
327 | var loaderNamespace;
328 | var loader = function(dispatcher, namespace) {
329 | loaderCalled = true;
330 | loaderDispatcher = dispatcher;
331 | loaderNamespace = namespace;
332 | };
333 | d.registerLoader('foo', loader);
334 | d.dispatch({action: 'bar.baz', event: mockEvent});
335 |
336 | assertFalse(loaderCalled);
337 | assertUndefined(loaderDispatcher);
338 | assertUndefined(loaderNamespace);
339 | }
340 |
341 |
342 | function testMaybeInvokeLoaderWithoutNamespaceLoader() {
343 | var d = new jsaction.Dispatcher;
344 | var mockEvent = jsaction.createEvent({type: 'click'});
345 | var defaultLoaderCalled = false;
346 | var defaultLoaderDispatcher;
347 | var defaultLoaderNamespace;
348 | var defaultLoader = function(dispatcher, namespace) {
349 | defaultLoaderCalled = true;
350 | defaultLoaderDispatcher = dispatcher;
351 | defaultLoaderNamespace = namespace;
352 | };
353 | d.registerDefaultLoader(defaultLoader);
354 | d.dispatch({action: 'foo.bar', event: mockEvent});
355 |
356 | assertTrue(defaultLoaderCalled);
357 | assertEquals(d, defaultLoaderDispatcher);
358 | assertEquals('foo', defaultLoaderNamespace);
359 | }
360 |
361 |
362 | function testMaybeInvokeLoaderWithNamespaceLoaderAndDefault() {
363 | var d = new jsaction.Dispatcher;
364 | var mockEvent = jsaction.createEvent({type: 'click'});
365 | var loaderCalled = false;
366 | var loaderDispatcher;
367 | var loaderNamespace;
368 | var loader = function(dispatcher, namespace) {
369 | loaderCalled = true;
370 | loaderDispatcher = dispatcher;
371 | loaderNamespace = namespace;
372 | };
373 | d.registerLoader('foo', loader);
374 |
375 | var defaultLoaderCalled = false;
376 | var defaultLoaderDispatcher;
377 | var defaultLoaderNamespace;
378 | var defaultLoader = function(dispatcher, namespace) {
379 | defaultLoaderCalled = true;
380 | defaultLoaderDispatcher = dispatcher;
381 | defaultLoaderNamespace = namespace;
382 | };
383 | d.registerDefaultLoader(defaultLoader);
384 |
385 | d.dispatch({action: 'foo.bar', event: mockEvent});
386 |
387 | assertTrue(loaderCalled);
388 | assertEquals(d, loaderDispatcher);
389 | assertEquals('foo', loaderNamespace);
390 | assertFalse(defaultLoaderCalled);
391 | assertUndefined(defaultLoaderDispatcher);
392 | assertUndefined(defaultLoaderNamespace);
393 | }
394 |
395 |
396 | function testMaybeInvokeLoaderWithDefaultRunsOnlyOnce() {
397 | var d = new jsaction.Dispatcher;
398 | var mockEvent = jsaction.createEvent({type: 'click'});
399 | var defaultLoaderCalled = false;
400 | var defaultLoaderCalledTimes = 0;
401 | var defaultLoaderDispatcher;
402 | var defaultLoaderNamespace;
403 | var defaultLoader = function(dispatcher, namespace) {
404 | defaultLoaderCalled = true;
405 | defaultLoaderCalledTimes++;
406 | defaultLoaderDispatcher = dispatcher;
407 | defaultLoaderNamespace = namespace;
408 | };
409 | d.registerDefaultLoader(defaultLoader);
410 | d.dispatch({action: 'foo.bar', event: mockEvent});
411 | // Default loader should be skipped the second time.
412 | d.dispatch({action: 'foo.bar', event: mockEvent});
413 |
414 | assertTrue(defaultLoaderCalled);
415 | assertEquals(1, defaultLoaderCalledTimes);
416 | assertEquals(d, defaultLoaderDispatcher);
417 | assertEquals('foo', defaultLoaderNamespace);
418 | }
419 |
420 |
421 | function testNamespaceDispatcherWithAccept() {
422 | var handler = goog.testing.recordFunction();
423 | var accept = mockControl_.createFunctionMock();
424 | accept(isObject_).$returns(false);
425 | accept(isObject_).$returns(true);
426 |
427 | mockControl_.$replayAll();
428 |
429 | var d = new jsaction.Dispatcher();
430 | d.registerNamespaceHandler('r', handler, accept);
431 | assertTrue(d.hasAction('r.abcd'));
432 |
433 | // accept() returns false.
434 | var mockEvent = jsaction.createEvent({type: 'click'});
435 | var eventInfo = {action: 'r.abcd', event: mockEvent};
436 | d.dispatch(eventInfo);
437 |
438 | assertEquals(0, handler.getCallCount());
439 |
440 | // accept() returns true.
441 | d.dispatch(eventInfo);
442 |
443 | assertEquals(1, handler.getCallCount());
444 |
445 | mockControl_.$verifyAll();
446 | }
447 |
448 |
449 | function testNamespaceDispatcherWithoutAccept() {
450 | var handler = goog.testing.recordFunction();
451 |
452 | mockControl_.$replayAll();
453 |
454 | var d = new jsaction.Dispatcher();
455 | d.registerNamespaceHandler('r', handler);
456 | assertTrue(d.hasAction('r.abcd'));
457 |
458 | var mockEvent = jsaction.createEvent({type: 'click'});
459 | var eventInfo = {action: 'r.abcd', event: mockEvent};
460 | d.dispatch(eventInfo);
461 |
462 | assertEquals(1, handler.getCallCount());
463 |
464 | mockControl_.$verifyAll();
465 | }
466 |
467 |
468 | function testCanDispatch() {
469 | var d = new jsaction.Dispatcher;
470 | d.registerHandlers('test', null, {'foo': function() {}});
471 | d.registerNamespaceHandler('ns', function() {}, goog.functions.TRUE);
472 | d.registerNamespaceHandler('ns2', function() {}, goog.functions.FALSE);
473 |
474 | assertTrue(d.canDispatch({action: 'test.foo'}));
475 | assertFalse(d.canDispatch({action: 'test.bar'}));
476 | assertFalse(d.canDispatch({action: 'nohandler.baz'}));
477 |
478 | assertTrue(d.canDispatch({action: 'ns.foo'}));
479 | assertTrue(d.canDispatch({action: 'ns.bar'}));
480 | assertFalse(d.canDispatch({action: 'ns2.foo'}));
481 | assertFalse(d.canDispatch({action: 'ns2.bar'}));
482 | }
483 |
484 | function testGlobalDispatch() {
485 | var handler = goog.testing.recordFunction();
486 |
487 | var d = new jsaction.Dispatcher;
488 | d.registerGlobalHandler('click', handler);
489 |
490 | var mockEvent = jsaction.createEvent({type: 'click'});
491 | var eventInfo = {event: mockEvent, eventType: 'click'};
492 | d.dispatch(eventInfo, true);
493 |
494 | assertEquals(1, handler.getCallCount());
495 | }
496 |
497 | function testGlobalDispatchSkipsHandlersForDifferentEventType() {
498 | var handler = goog.testing.recordFunction();
499 |
500 | var d = new jsaction.Dispatcher;
501 | d.registerGlobalHandler('click', handler);
502 |
503 | var mockEvent = jsaction.createEvent({type: 'mousedown'});
504 | var eventInfo = {event: mockEvent, eventType: 'mousedown'};
505 | d.dispatch(eventInfo, true);
506 |
507 | assertEquals(0, handler.getCallCount());
508 | }
509 |
510 | function testDispatchSetWiz() {
511 | var eventInfo = {action: 'foo.bar', eventType: 'click'};
512 | var d = new jsaction.Dispatcher(null, null, true);
513 | var actions = {'bar': function() {}};
514 | d.registerHandlers('foo', null, actions);
515 |
516 | var mockSetWiz = mockControl_.createMethodMock(jsaction.ActionFlow.prototype,
517 | 'setWiz');
518 | mockSetWiz().$times(1);
519 |
520 | mockControl_.$replayAll();
521 | d.dispatch(eventInfo);
522 | mockControl_.$verifyAll();
523 | }
524 |
525 | function testDispatchNotSetWiz() {
526 | var eventInfo = {action: 'foo.bar', eventType: 'click'};
527 | var d = new jsaction.Dispatcher();
528 | var actions = {'bar': function() {}};
529 | d.registerHandlers('foo', null, actions);
530 |
531 | var mockSetWiz = mockControl_.createMethodMock(jsaction.ActionFlow.prototype,
532 | 'setWiz');
533 | mockSetWiz().$times(0);
534 |
535 | mockControl_.$replayAll();
536 | d.dispatch(eventInfo);
537 | mockControl_.$verifyAll();
538 | }
539 |
--------------------------------------------------------------------------------
/dom.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc. All Rights Reserved.
2 |
3 | /**
4 | * @fileoverview Functions that help jsaction interact with the DOM. We
5 | * deliberately don't use the closure equivalents here because we want
6 | * to exercise very tight control over the dependencies.
7 | */
8 | goog.provide('jsaction.dom');
9 |
10 |
11 | /**
12 | * Determines if one node is contained within another. Adapted from
13 | * {@see goog.dom.contains}.
14 | * @param {!Node} node Node that should contain otherNode.
15 | * @param {Node} otherNode Node being contained.
16 | * @return {boolean} True if otherNode is contained within node.
17 | */
18 | jsaction.dom.contains = function(node, otherNode) {
19 | if (otherNode === null) {
20 | return false;
21 | }
22 |
23 | // We use browser specific methods for this if available since it is faster
24 | // that way.
25 |
26 | // IE DOM
27 | if ('contains' in node && otherNode.nodeType == 1) {
28 | return node.contains(otherNode);
29 | }
30 |
31 | // W3C DOM Level 3
32 | if ('compareDocumentPosition' in node) {
33 | return node == otherNode ||
34 | Boolean(node.compareDocumentPosition(otherNode) & 16);
35 | }
36 |
37 | // W3C DOM Level 1
38 | while (otherNode && node != otherNode) {
39 | otherNode = otherNode.parentNode;
40 | }
41 | return otherNode == node;
42 | };
43 |
44 | /**
45 | * Helper method for broadcastCustomEvent. Returns true if any member of
46 | * the set is an ancestor of element.
47 | *
48 | * @param {!Element} element
49 | * @param {!NodeList} nodeList
50 | * @return {boolean}
51 | */
52 | jsaction.dom.hasAncestorInNodeList = function(element, nodeList) {
53 | for (let idx = 0; idx < nodeList.length; ++idx) {
54 | const member = nodeList[idx];
55 | if (member != element && jsaction.dom.contains(member, element)) {
56 | return true;
57 | }
58 | }
59 | return false;
60 | };
61 |
--------------------------------------------------------------------------------
/dom_test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc. All Rights Reserved.
2 |
3 |
4 | /** @suppress {extraProvide} */
5 | goog.provide('jsaction.domTest');
6 | goog.setTestOnly('jsaction.domTest');
7 |
8 | goog.require('goog.testing.jsunit');
9 | goog.require('jsaction.dom');
10 |
11 |
12 | function testContains() {
13 | var root = document.createElement('div');
14 | var child = document.createElement('div');
15 | var subchild = document.createElement('div');
16 | child.appendChild(subchild);
17 | root.appendChild(child);
18 |
19 | assertTrue(jsaction.dom.contains(root, root));
20 | assertTrue(jsaction.dom.contains(root, child));
21 | assertTrue(jsaction.dom.contains(root, subchild));
22 | assertTrue(jsaction.dom.contains(child, subchild));
23 | assertFalse(jsaction.dom.contains(subchild, root));
24 | assertFalse(jsaction.dom.contains(subchild, child));
25 | }
26 |
--------------------------------------------------------------------------------
/event_test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All rights reserved.
2 |
3 | /**
4 | */
5 |
6 | /** @suppress {extraProvide} */
7 | goog.provide('jsaction.eventTest');
8 | goog.setTestOnly('jsaction.eventTest');
9 |
10 | goog.require('goog.dom');
11 | goog.require('goog.dom.TagName');
12 | goog.require('goog.testing.events.Event');
13 | goog.require('goog.testing.jsunit');
14 | goog.require('jsaction.EventType');
15 | goog.require('jsaction.KeyCodes');
16 | goog.require('jsaction.event');
17 |
18 |
19 | function DivMock() {
20 | this.listeners = [];
21 | }
22 |
23 |
24 | DivMock.prototype.addEventListener = function(event, handler, capture) {
25 | this.listeners.push([0, event, handler, capture]);
26 | };
27 |
28 |
29 | DivMock.prototype.attachEvent = function(event, handler) {
30 | this.listeners.push([1, event, handler]);
31 | };
32 |
33 |
34 | var div_ = null;
35 | var validTarget =
36 | goog.dom.createDom(goog.dom.TagName.DIV, {tabIndex: 0, role: 'button'});
37 | var invalidTarget = document.createElement('div');
38 | var roleTarget =
39 | goog.dom.createDom(goog.dom.TagName.DIV, {tabIndex: 0, role: 'textbox'});
40 |
41 |
42 | function setUp() {
43 | div_ = new DivMock;
44 | }
45 |
46 |
47 | function testAddEventListenerW3C() {
48 | var eventInfo = jsaction.event.addEventListener(
49 | div_, 'click', goog.nullFunction);
50 | assertEquals('click', eventInfo.eventType);
51 | assertFalse(eventInfo.capture);
52 | }
53 |
54 |
55 | function testAddEventListenerIE() {
56 | div_.addEventListener = null;
57 | var handlerThis = null;
58 | var handler = function() {
59 | handlerThis = this;
60 | };
61 |
62 | var eventInfo = jsaction.event.addEventListener(div_, 'click', handler);
63 | assertEquals('click', eventInfo.eventType);
64 | assertFalse(handler == eventInfo.handler);
65 |
66 | eventInfo.handler();
67 | assertEquals(div_, handlerThis);
68 | }
69 |
70 |
71 | function testAddEventListenerFocusW3C() {
72 | var eventInfo = jsaction.event.addEventListener(
73 | div_, 'focus', goog.nullFunction);
74 | assertEquals('focus', eventInfo.eventType);
75 | assertTrue(eventInfo.capture);
76 | }
77 |
78 |
79 | function testAddEventListenerBlurW3C() {
80 | var eventInfo = jsaction.event.addEventListener(
81 | div_, 'blur', goog.nullFunction);
82 | assertEquals('blur', eventInfo.eventType);
83 | assertTrue(eventInfo.capture);
84 | }
85 |
86 |
87 | function testAddEventListenerErrorW3C() {
88 | var eventInfo = jsaction.event.addEventListener(
89 | div_, 'error', goog.nullFunction);
90 | assertEquals('error', eventInfo.eventType);
91 | assertTrue(eventInfo.capture);
92 | }
93 |
94 |
95 | function testAddEventListenerLoadW3C() {
96 | var eventInfo = jsaction.event.addEventListener(
97 | div_, 'load', goog.nullFunction);
98 | assertEquals('load', eventInfo.eventType);
99 | assertTrue(eventInfo.capture);
100 | }
101 |
102 |
103 | function testAddEventListenerFocusIE() {
104 | div_.addEventListener = null;
105 | var eventInfo = jsaction.event.addEventListener(
106 | div_, 'focus', goog.nullFunction);
107 | assertEquals('focusin', eventInfo.eventType);
108 | }
109 |
110 |
111 | function testAddEventListenerBlurIE() {
112 | div_.addEventListener = null;
113 | var eventInfo = jsaction.event.addEventListener(
114 | div_, 'blur', goog.nullFunction);
115 | assertEquals('focusout', eventInfo.eventType);
116 | }
117 |
118 |
119 | function testIsModifiedClickEventMacMetaKey() {
120 | var event = {metaKey: true};
121 | jsaction.event.isMac_ = true;
122 | assertTrue(jsaction.event.isModifiedClickEvent(event));
123 | }
124 |
125 |
126 | function testIsModifiedClickEventNonMacCtrlKey() {
127 | var event = {ctrlKey: true};
128 | jsaction.event.isMac_ = false;
129 | assertTrue(jsaction.event.isModifiedClickEvent(event));
130 | }
131 |
132 |
133 | function testIsModifiedClickEventMiddleClick() {
134 | var event = {which: 2};
135 | assertTrue(jsaction.event.isModifiedClickEvent(event));
136 | }
137 |
138 |
139 | function testIsModifiedClickEventMiddleClickIE() {
140 | var event = {button: 4};
141 | assertTrue(jsaction.event.isModifiedClickEvent(event));
142 | }
143 |
144 |
145 | function testIsModifiedClickEventShiftKey() {
146 | var event = {shiftKey: true};
147 | assertTrue(jsaction.event.isModifiedClickEvent(event));
148 | }
149 |
150 |
151 | function testIsValidActionKeyTarget() {
152 | var div = document.createElement('div');
153 | div.setAttribute('role', 'checkbox');
154 | var textarea = document.createElement('textarea');
155 | var input = document.createElement('input');
156 | input.type = 'password';
157 | assertTrue(jsaction.event.isValidActionKeyTarget_(div));
158 | assertFalse(jsaction.event.isValidActionKeyTarget_(textarea));
159 | assertFalse(jsaction.event.isValidActionKeyTarget_(input));
160 | input.setAttribute('role', 'combobox');
161 | assertEquals('combobox', input.getAttribute('role'));
162 | assertFalse(jsaction.event.isValidActionKeyTarget_(input));
163 | var search = document.createElement('search');
164 | search.type = 'search';
165 | assertEquals('search', search.type);
166 | assertFalse(jsaction.event.isValidActionKeyTarget_(search));
167 | var holder = document.createElement('div');
168 | holder.innerHTML = ' ';
169 | var num = holder.firstChild;
170 | assertEquals('number', num.getAttribute('type'));
171 | assertFalse(jsaction.event.isValidActionKeyTarget_(num));
172 |
173 | var div2 = document.createElement('div');
174 | // contentEditable only works on non-orphaned elements.
175 | document.body.appendChild(div2);
176 | div2.contentEditable = 'true';
177 | div2.setAttribute('role', 'combobox');
178 | assertFalse(jsaction.event.isValidActionKeyTarget_(div2));
179 | div2.removeAttribute('role');
180 | assertFalse(jsaction.event.isValidActionKeyTarget_(div2));
181 | div.removeAttribute('role');
182 | assertTrue(jsaction.event.isValidActionKeyTarget_(div));
183 | document.body.removeChild(div2);
184 | }
185 |
186 |
187 | function testIsActionKeyEventFailsOnClick() {
188 | var event = {
189 | type: 'click',
190 | target: validTarget
191 | };
192 | assertFalse(jsaction.event.isActionKeyEvent(event));
193 | }
194 |
195 |
196 | function baseIsActionKeyEvent(keyCode, opt_target, opt_originalTarget) {
197 | var event = {
198 | type: jsaction.EventType.KEYDOWN,
199 | which: keyCode,
200 | target: opt_target || validTarget,
201 | originalTarget: opt_originalTarget || opt_target || validTarget
202 | };
203 |
204 | try {
205 | // isFocusable() in IE calls getBoundingClientRect(), which fails on orphans
206 | document.body.appendChild(event.target);
207 | event.target.style.height = '4px'; // Make sure we don't report as hidden.
208 | event.target.style.width = '4px';
209 | return jsaction.event.isActionKeyEvent(event);
210 | } finally {
211 | document.body.removeChild(event.target);
212 | }
213 | }
214 |
215 |
216 | function testIsActionKeyEventFailsOnInvalidKey() {
217 | assertFalse(baseIsActionKeyEvent(64));
218 | }
219 |
220 |
221 | function testIsActionKeyEventEnter() {
222 | assertTrue(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER));
223 | }
224 |
225 |
226 | function testIsActionKeyEventSpace() {
227 | assertTrue(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE));
228 | }
229 |
230 |
231 | function testIsActionKeyRealCheckBox() {
232 | var checkbox = document.createElement('input');
233 | checkbox.type = 'checkbox';
234 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE, checkbox));
235 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, checkbox));
236 | }
237 |
238 |
239 | function testIsActionKeyFakeCheckBox() {
240 | var checkbox =
241 | goog.dom.createDom(goog.dom.TagName.DIV, {tabIndex: 0, role: 'checkbox'});
242 | assertTrue(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE, checkbox));
243 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, checkbox));
244 | }
245 |
246 |
247 | function testIsActionKeyEventMacEnter() {
248 | if (!jsaction.event.isWebKit_) {
249 | return;
250 | }
251 | assertTrue(baseIsActionKeyEvent(jsaction.KeyCodes.MAC_ENTER));
252 | }
253 |
254 | function testIsActionKeyNonControl() {
255 | var control = goog.dom.createDom(goog.dom.TagName.DIV);
256 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
257 | }
258 |
259 | function testIsActionKeyDisabledControl() {
260 | var control = goog.dom.createDom(goog.dom.TagName.BUTTON, {disabled: true});
261 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
262 | }
263 |
264 | function testIsActionKeyNonTabbableControl() {
265 | let control = goog.dom.createDom(goog.dom.TagName.DIV);
266 | // Adding role=button will make jsaction treat the div (normally not
267 | // interactable) as a control, although it will remain non-tabbable.
268 | control.setAttribute('role', 'button');
269 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
270 | }
271 |
272 | function testIsActionKeyNativelyActivatableControl() {
273 | var control = goog.dom.createDom(goog.dom.TagName.BUTTON);
274 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE, control));
275 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
276 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.MAC_ENTER, control));
277 | }
278 |
279 | function testIsActionKeyFileInput() {
280 | var control = goog.dom.createDom(goog.dom.TagName.INPUT, {type: 'file'});
281 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE, control));
282 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
283 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.MAC_ENTER, control));
284 | }
285 |
286 | function testIsActionKeyEventNotInMap() {
287 | var control = goog.dom.createDom(goog.dom.TagName.DIV, {tabIndex: 0});
288 | assertTrue(baseIsActionKeyEvent(jsaction.KeyCodes.ENTER, control));
289 | assertFalse(baseIsActionKeyEvent(jsaction.KeyCodes.SPACE, control));
290 | }
291 |
292 | function testIsMouseSpecialEventMouseenter() {
293 | var root = document.createElement('div');
294 | var child = document.createElement('div');
295 | root.appendChild(child);
296 |
297 | var event = {
298 | relatedTarget: root,
299 | type: jsaction.EventType.MOUSEOVER,
300 | target: child
301 | };
302 |
303 | assertTrue(jsaction.event.isMouseSpecialEvent(event,
304 | jsaction.EventType.MOUSEENTER, child));
305 | }
306 |
307 | function testIsMouseSpecialEventNotMouseenter() {
308 | var root = document.createElement('div');
309 | var child = document.createElement('div');
310 | root.appendChild(child);
311 |
312 | var event = {
313 | relatedTarget: child,
314 | type: jsaction.EventType.MOUSEOVER,
315 | target: root
316 | };
317 |
318 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
319 | jsaction.EventType.MOUSEENTER, root));
320 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
321 | jsaction.EventType.MOUSEENTER, child));
322 | }
323 |
324 | function testIsMouseSpecialEventMouseover() {
325 | var root = document.createElement('div');
326 | var child = document.createElement('div');
327 | root.appendChild(child);
328 | var subchild = document.createElement('div');
329 | child.appendChild(subchild);
330 |
331 | var event = {
332 | relatedTarget: child,
333 | type: jsaction.EventType.MOUSEOVER,
334 | target: subchild
335 | };
336 |
337 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
338 | jsaction.EventType.MOUSEENTER, root));
339 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
340 | jsaction.EventType.MOUSEENTER, child));
341 | assertTrue(jsaction.event.isMouseSpecialEvent(event,
342 | jsaction.EventType.MOUSEENTER, subchild));
343 | }
344 |
345 | function testIsMouseSpecialEventMouseleave() {
346 | var root = document.createElement('div');
347 | var child = document.createElement('div');
348 | root.appendChild(child);
349 |
350 | var event = {
351 | relatedTarget: root,
352 | type: jsaction.EventType.MOUSEOUT,
353 | target: child
354 | };
355 |
356 | assertTrue(jsaction.event.isMouseSpecialEvent(event,
357 | jsaction.EventType.MOUSELEAVE, child));
358 | }
359 |
360 | function testIsMouseSpecialEventNotMouseleave() {
361 | var root = document.createElement('div');
362 | var child = document.createElement('div');
363 | root.appendChild(child);
364 |
365 | var event = {
366 | relatedTarget: child,
367 | type: jsaction.EventType.MOUSEOUT,
368 | target: root
369 | };
370 |
371 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
372 | jsaction.EventType.MOUSELEAVE, root));
373 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
374 | jsaction.EventType.MOUSELEAVE, child));
375 | }
376 |
377 | function testIsMouseSpecialEventMouseout() {
378 | var root = document.createElement('div');
379 | var child = document.createElement('div');
380 | root.appendChild(child);
381 | var subchild = document.createElement('div');
382 | child.appendChild(subchild);
383 |
384 | var event = {
385 | relatedTarget: child,
386 | type: jsaction.EventType.MOUSEOUT,
387 | target: subchild
388 | };
389 |
390 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
391 | jsaction.EventType.MOUSELEAVE, root));
392 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
393 | jsaction.EventType.MOUSELEAVE, child));
394 | assertTrue(jsaction.event.isMouseSpecialEvent(event,
395 | jsaction.EventType.MOUSELEAVE, subchild));
396 | }
397 |
398 | function testIsMouseSpecialEventNotMouse() {
399 | var root = document.createElement('div');
400 | var child = document.createElement('div');
401 | root.appendChild(child);
402 |
403 | var event = {
404 | relatedTarget: root,
405 | type: jsaction.EventType.CLICK,
406 | target: child
407 | };
408 |
409 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
410 | jsaction.EventType.MOUSELEAVE, child));
411 | assertFalse(jsaction.event.isMouseSpecialEvent(event,
412 | jsaction.EventType.MOUSELEAVE, child));
413 | }
414 |
415 | function testCreateMouseSpecialEventMouseenter() {
416 | var div = document.createElement('div');
417 | var event = new goog.testing.events.Event(jsaction.EventType.MOUSEOVER, div);
418 | var copiedEvent = jsaction.event.createMouseSpecialEvent(event, div);
419 | assertEquals(jsaction.EventType.MOUSEENTER, copiedEvent['type']);
420 | assertEquals(div, copiedEvent['target']);
421 | assertEquals(false, copiedEvent['bubbles']);
422 | }
423 |
424 | function testCreateMouseSpecialEventMouseleave() {
425 | var div = document.createElement('div');
426 | var event = new goog.testing.events.Event(jsaction.EventType.MOUSEOUT, div);
427 | var copiedEvent = jsaction.event.createMouseSpecialEvent(event, div);
428 | assertEquals(jsaction.EventType.MOUSELEAVE, copiedEvent['type']);
429 | assertEquals(div, copiedEvent['target']);
430 | assertEquals(false, copiedEvent['bubbles']);
431 | }
432 |
433 | function testRecreateTouchEventAsClick() {
434 | var div = document.createElement('div');
435 | var origEvent = new goog.testing.events.Event('touchend', div);
436 | origEvent.touches = [{
437 | clientX: 1,
438 | clientY: 2,
439 | screenX: 3,
440 | screenY: 4,
441 | pageX: 5,
442 | pageY: 6
443 | }, {}];
444 | var event = jsaction.event.recreateTouchEventAsClick(origEvent);
445 | assertEquals('click', event.type);
446 | assertEquals(1, event.clientX);
447 | assertEquals(2, event.clientY);
448 | assertEquals(3, event.screenX);
449 | assertEquals(4, event.screenY);
450 |
451 | origEvent = new goog.testing.events.Event('touchend', div);
452 | origEvent.changedTouches = [{
453 | clientX: 'other',
454 | clientY: 2,
455 | screenX: 3,
456 | screenY: 4,
457 | pageX: 5,
458 | pageY: 6
459 | }];
460 | assertEquals('touchend', origEvent.type);
461 | event = jsaction.event.recreateTouchEventAsClick(origEvent);
462 | assertEquals('click', event.type);
463 | assertEquals('other', event.clientX);
464 | assertEquals(2, event.clientY);
465 | assertEquals(3, event.screenX);
466 | assertEquals(4, event.screenY);
467 | assertEquals('touchend', event.originalEventType);
468 |
469 | origEvent = new goog.testing.events.Event('touchend', div);
470 | origEvent.changedTouches = [];
471 | origEvent.touches = [{
472 | clientX: 1
473 | }, {}];
474 | event = jsaction.event.recreateTouchEventAsClick(origEvent);
475 | assertEquals('click', event.type);
476 | assertEquals(1, event.clientX);
477 | }
478 |
479 | function testRecreateTouchEventAsClick_hasTouchData() {
480 | var div = document.createElement('div');
481 | var event = new goog.testing.events.Event(jsaction.EventType.TOUCHEND, div);
482 | event['touches'] = [{
483 | 'clientX': 101,
484 | 'clientY': 102,
485 | 'screenX': 201,
486 | 'screenY': 202
487 | }];
488 | var copiedEvent = jsaction.event.recreateTouchEventAsClick(event);
489 | assertEquals(jsaction.EventType.CLICK, copiedEvent['type']);
490 | assertEquals(jsaction.EventType.TOUCHEND, copiedEvent['originalEventType']);
491 | assertEquals(div, copiedEvent['target']);
492 | assertEquals(101, copiedEvent['clientX']);
493 | assertEquals(102, copiedEvent['clientY']);
494 | assertEquals(201, copiedEvent['screenX']);
495 | assertEquals(202, copiedEvent['screenY']);
496 | }
497 |
498 | function testRecreateTouchEventAsClick_noTouchData() {
499 | var div = document.createElement('div');
500 | var event = new goog.testing.events.Event(jsaction.EventType.TOUCHEND, div);
501 | var copiedEvent = jsaction.event.recreateTouchEventAsClick(event);
502 | assertEquals(jsaction.EventType.CLICK, copiedEvent['type']);
503 | assertEquals(jsaction.EventType.TOUCHEND, copiedEvent['originalEventType']);
504 | assertEquals(div, copiedEvent['target']);
505 | assertUndefined(copiedEvent['clientX']);
506 | assertUndefined(copiedEvent['clientY']);
507 | assertUndefined(copiedEvent['screenX']);
508 | assertUndefined(copiedEvent['screenY']);
509 | }
510 |
511 | function testRecreateTouchEventAsClick_behavior() {
512 | var div = document.createElement('div');
513 | var origEvent = new goog.testing.events.Event('touchend', div);
514 | origEvent.touches = [{
515 | clientX: 1,
516 | clientY: 2,
517 | screenX: 3,
518 | screenY: 4,
519 | pageX: 5,
520 | pageY: 6
521 | }, {}];
522 | var event = jsaction.event.recreateTouchEventAsClick(origEvent);
523 | assertEquals('click', event.type);
524 |
525 | assertFalse(event.defaultPrevented);
526 | event.preventDefault();
527 | assertTrue(event.defaultPrevented);
528 |
529 | assertFalse(event['_propagationStopped']);
530 | event.stopPropagation();
531 | assertTrue(event['_propagationStopped']);
532 | }
533 |
534 | function testRecreateTouchEventAsClick_timeStamp() {
535 | var div = document.createElement('div');
536 | var origEvent = new goog.testing.events.Event('touchend', div);
537 | origEvent.touches = [{
538 | clientX: 1,
539 | clientY: 2,
540 | screenX: 3,
541 | screenY: 4,
542 | pageX: 5,
543 | pageY: 6
544 | }, {}];
545 | var event = jsaction.event.recreateTouchEventAsClick(origEvent);
546 | assertEquals('click', event.type);
547 | assertTrue(event.timeStamp >= goog.now() - 500);
548 | }
549 |
550 | function testPreventMouseEvents() {
551 | var div = document.createElement('div');
552 | var event = new goog.testing.events.Event('touchend', div);
553 |
554 | assertFalse(jsaction.event.isMouseEventsPrevented(event));
555 |
556 | jsaction.event.preventMouseEvents(event);
557 | assertTrue(jsaction.event.isMouseEventsPrevented(event));
558 | }
559 |
560 | function testAddPreventMouseEventsSupport() {
561 | var div = document.createElement('div');
562 | var event = new goog.testing.events.Event('touchend', div);
563 | jsaction.event.addPreventMouseEventsSupport(event);
564 |
565 | assertFalse(jsaction.event.isMouseEventsPrevented(event));
566 |
567 | event['_preventMouseEvents']();
568 | assertTrue(jsaction.event.isMouseEventsPrevented(event));
569 | }
570 |
571 | function testMaybeCopyEvent() {
572 | var div = document.createElement('div');
573 | document.body.appendChild(div);
574 | var event;
575 | var maybeCopy;
576 | div.onclick = function(e) {
577 | event = e || window.event;
578 | maybeCopy = jsaction.event.maybeCopyEvent(event);
579 | };
580 | if (document.createEvent) { // All browsers except older IEs.
581 | var toDispatch = document.createEvent('HTMLEvents');
582 | toDispatch.initEvent('click', true, true);
583 | div.dispatchEvent(toDispatch);
584 | } else {
585 | div.click();
586 | }
587 | assertNotNullNorUndefined(event);
588 | if (document.createEvent) {
589 | assertEquals(event, maybeCopy);
590 | } else {
591 | assertNotEquals(event, maybeCopy);
592 | }
593 | if (maybeCopy.target) {
594 | assertEquals(div, maybeCopy.target);
595 | } else {
596 | assertEquals(div, maybeCopy.srcElement);
597 | }
598 | }
599 |
600 |
601 | function testMaybeCopyEventDoesNotCopyNonBrowserEvent() {
602 | var event = {};
603 | var maybeCopy = jsaction.event.maybeCopyEvent(event);
604 | assertEquals(event, maybeCopy);
605 | // More browser like:
606 | var node = document.createElement('div');
607 | event = {
608 | type: 'click',
609 | target: node,
610 | srcElement: node
611 | };
612 | maybeCopy = jsaction.event.maybeCopyEvent(event);
613 | assertEquals(event, maybeCopy);
614 | }
615 |
616 |
617 | function testIsSpaceKeyEvent() {
618 | var ev = {
619 | target: validTarget,
620 | keyCode: jsaction.KeyCodes.SPACE
621 | };
622 | assertTrue(jsaction.event.isSpaceKeyEvent(ev));
623 | var input = goog.dom.createDom(goog.dom.TagName.INPUT);
624 | input.type = 'checkbox';
625 | ev = {
626 | target: input,
627 | keyCode: jsaction.KeyCodes.SPACE
628 | };
629 | assertFalse(jsaction.event.isSpaceKeyEvent(ev));
630 | }
631 |
632 |
633 | function testShouldCallPreventDefaultOnNativeHtmlControl() {
634 | var ev = {
635 | target: validTarget
636 | };
637 | assertTrue(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
638 | ev = {
639 | target: invalidTarget
640 | };
641 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
642 | ev = {
643 | target: roleTarget
644 | };
645 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
646 | var button = document.createElement('button');
647 | ev = {
648 | target: button
649 | };
650 | assertTrue(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
651 | var divWithButtonRole = document.createElement('div');
652 | divWithButtonRole.setAttribute('role', 'button');
653 | ev = {
654 | target: divWithButtonRole
655 | };
656 | assertTrue(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
657 | var input = document.createElement('input');
658 | input.type = 'button';
659 | ev = {
660 | target: input
661 | };
662 | assertTrue(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
663 | var checkbox = document.createElement('input');
664 | checkbox.type = 'checkbox';
665 | ev = {
666 | target: checkbox
667 | };
668 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
669 | var radio = document.createElement('input');
670 | radio.type = 'radio';
671 | ev = {
672 | target: radio
673 | };
674 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
675 | var select = document.createElement('select');
676 | ev = {
677 | target: select
678 | };
679 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
680 | var option = document.createElement('option');
681 | ev = {
682 | target: option
683 | };
684 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
685 | var link = document.createElement('a');
686 | link.setAttribute('href', 'http://www.google.com');
687 | ev = {
688 | target: link
689 | };
690 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
691 | var linkWithRole = document.createElement('a');
692 | linkWithRole.setAttribute('href', 'http://www.google.com');
693 | linkWithRole.setAttribute('role', 'menuitem');
694 | ev = {
695 | target: linkWithRole
696 | };
697 | assertFalse(jsaction.event.shouldCallPreventDefaultOnNativeHtmlControl(ev));
698 | }
699 |
--------------------------------------------------------------------------------
/eventcontract_auto.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview The entry point for JsAction. This is mean to be inlined in
3 | * the main HTML page.
4 | *
5 | * It will automatically read a list of event types from an element containing
6 | * the data-jsaction-events attribute and create a container for it. This
7 | * element must be present in the DOM for event binding to succeed.
8 | */
9 |
10 | goog.provide('jsaction.eventContractAuto');
11 |
12 | goog.require('jsaction.EventContract');
13 |
14 |
15 | /**
16 | * Binds all events for a given JsAction container.
17 | * @param {!jsaction.EventContract} contract
18 | * @param {Element} container
19 | * @return {boolean} True, if events were successfully bound.
20 | */
21 | function bindEventsForContainer(contract, container) {
22 | if (container === null) {
23 | return false;
24 | }
25 | var eventTypes = container.getAttribute('data-jsaction-events');
26 | if (!eventTypes) {
27 | return false;
28 | }
29 | contract.addContainer(container);
30 |
31 | eventTypes = eventTypes.split(',');
32 | for (var i = 0, eventType; eventType = eventTypes[i++];) {
33 | contract.addEvent(eventType);
34 | }
35 | return true;
36 | }
37 |
38 |
39 | /**
40 | * Creates an event contract.
41 | * @param {!Object} global
42 | */
43 | function main(global) {
44 | var contract = new jsaction.EventContract();
45 | var container = document.querySelector('[data-jsaction-events]');
46 | if (bindEventsForContainer(contract, container)) {
47 | global['jsaction'] = {};
48 | global['jsaction']['__dispatchTo'] = function(dispatcher) {
49 | contract.dispatchTo(dispatcher);
50 | };
51 | }
52 | }
53 |
54 | // Bootstraps an event contract.
55 | main(goog.global);
56 |
--------------------------------------------------------------------------------
/eventcontract_example.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All Rights Reserved.
2 |
3 | /**
4 | *
5 | * @fileoverview The entry point for a jsbinary that instantiates
6 | * jsaction.EventContract and eventually passes it to an instance of
7 | * jsaction.Dispatcher. This is meant to be inlined in the main HTML
8 | * page.
9 | *
10 | * Cf. dispatcher_example.js, the external counterpart.
11 | *
12 | * This file serves as model for how Dispatcher and EventContract
13 | * cooperate, and to check that the code jscompiles properly.
14 | */
15 |
16 | goog.provide('jsaction.eventContractExample');
17 |
18 | goog.require('jsaction.EventContract');
19 |
20 |
21 | /**
22 | * This function should be executed right when the page loads this
23 | * code, which should be inline and right after the body.
24 | *
25 | * @param {Window} window The window of the page this event
26 | * contract handles.
27 | */
28 | function main(window) {
29 | var evc = new jsaction.EventContract;
30 | evc.addEvent('click');
31 | evc.addContainer(/** @type {!Element} */(window.document.body));
32 |
33 | // Cf. dispatcher_main.js.
34 | window['dispatcherOnLoad'] = function(dispatcher) {
35 | evc.dispatchTo(dispatcher);
36 | };
37 | }
38 |
39 | // The function is exported first such that loading the code and
40 | // executing it is decoupled, which makes for more
41 | // clarity. Specifically, the code could be inlined in head, but the
42 | // body can only be registered as a container for the event contract
43 | // after the start tag, because before that it doesn't exist.
44 | // Also, this prevents the code from being eliminated by jscompiler.
45 | window['main'] = main;
46 |
--------------------------------------------------------------------------------
/eventcontract_export.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview File that exports symbols from the event contract to be used
3 | * in standalone binaries that drop the event contract script into their page.
4 | */
5 |
6 | goog.provide('jsaction.eventContractExport');
7 |
8 | goog.require('jsaction.EventContract');
9 |
10 |
11 | goog.exportSymbol('jsaction.EventContract', jsaction.EventContract);
12 | goog.exportSymbol(
13 | 'jsaction.EventContract.prototype.addContainer',
14 | jsaction.EventContract.prototype.addContainer);
15 | goog.exportSymbol(
16 | 'jsaction.EventContract.prototype.addEvent',
17 | jsaction.EventContract.prototype.addEvent);
18 | goog.exportSymbol(
19 | 'jsaction.EventContract.prototype.dispatchTo',
20 | jsaction.EventContract.prototype.dispatchTo);
21 |
--------------------------------------------------------------------------------
/eventcontract_test_dom.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
40 |
41 |
42 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
79 |
80 |
81 |
82 |
83 |
90 |
91 |
92 |
97 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
119 |
120 |
126 |
127 |
133 |
134 |
138 |
139 |
143 |
144 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/generator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Contains the generic interface for iterating over the dom path
3 | * an event has traveled. These generators are meant to be singletons so you
4 | * should not construct them yourself. You should use the static factory method
5 | * getGenerator instead.
6 | */
7 | goog.provide('jsaction.domGenerator');
8 | goog.provide('jsaction.domGenerator.Ancestors');
9 | goog.provide('jsaction.domGenerator.EventPath');
10 | goog.provide('jsaction.domGenerator.Generator');
11 |
12 | goog.require('jsaction.Property');
13 |
14 |
15 |
16 | /** @interface */
17 | jsaction.domGenerator.Generator = function() {};
18 |
19 |
20 | /**
21 | * @return {Element} The next element in the generator or null if none found.
22 | */
23 | jsaction.domGenerator.Generator.prototype.next = function() {};
24 |
25 |
26 |
27 | /**
28 | * Constructs a generator of all the ancestors of an element.
29 | * @constructor
30 | * @implements {jsaction.domGenerator.Generator}
31 | */
32 | jsaction.domGenerator.Ancestors = function() {
33 | /** @private {?Element} */
34 | this.node_ = null;
35 |
36 | /** @private {?Element} */
37 | this.container_ = null;
38 | };
39 |
40 |
41 | /**
42 | * Resets an ancestors generator of an element with a new target and container.
43 | * @param {!Element} target the element to start walking ancestors at.
44 | * @param {!Element} container the element to stop walking ancestors at.
45 | * @return {!jsaction.domGenerator.Generator}
46 | * @private
47 | */
48 | jsaction.domGenerator.Ancestors.prototype.reset_ =
49 | function(target, container) {
50 | this.node_ = target;
51 | this.container_ = container;
52 | return this;
53 | };
54 |
55 |
56 | /** @override */
57 | jsaction.domGenerator.Ancestors.prototype.next = function() {
58 | // Walk to the parent node, unless the node has a different owner in
59 | // which case we walk to the owner.
60 | const curr = this.node_;
61 | if (this.node_ && this.node_ != this.container_) {
62 | this.node_ = this.node_[jsaction.Property.OWNER] || this.node_.parentNode;
63 | } else {
64 | this.node_ = null;
65 | }
66 |
67 | return curr;
68 | };
69 |
70 |
71 |
72 | /**
73 | * Constructs a generator of all elements in a path array.
74 | * Correctly handles jsaction.Property.OWNER on elements.
75 | * @constructor
76 | * @implements {jsaction.domGenerator.Generator}
77 | */
78 | jsaction.domGenerator.EventPath = function() {
79 | /** @private {!Array.} */
80 | this.path_ = [];
81 |
82 | /** @private {number} */
83 | this.idx_ = 0;
84 |
85 | /** @private {?Element} */
86 | this.container_ = null;
87 |
88 | /** @private {boolean} */
89 | this.usingAncestors_ = false;
90 | };
91 |
92 |
93 | /**
94 | * Resets an EventPath with a new path and container.
95 | * @param {!Array.} path
96 | * @param {!Element} container
97 | * @return {!jsaction.domGenerator.Generator}
98 | * @private
99 | */
100 | jsaction.domGenerator.EventPath.prototype.reset_ =
101 | function(path, container) {
102 | this.path_ = path;
103 | this.idx_ = 0;
104 | this.container_ = container;
105 | this.usingAncestors_ = false;
106 | return this;
107 | };
108 |
109 |
110 | /** @override */
111 | jsaction.domGenerator.EventPath.prototype.next = function() {
112 | // TODO(user): If we could ban OWNERS for all users of event.path
113 | // then you could greatly simplify the code here.
114 | if (this.usingAncestors_) {
115 | return jsaction.domGenerator.ancestors_.next();
116 | }
117 | if (this.idx_ != this.path_.length) {
118 | const curr = this.path_[this.idx_];
119 | this.idx_++;
120 | if (curr != this.container_) {
121 | // NOTE(user): The presence of the OWNER property indicates that
122 | // the user wants to override the browsers expected event path with
123 | // one of their own. The eventpath generator still needs to respect
124 | // the OWNER property since this is used by a lot of jsactions
125 | // consumers.
126 | if (curr && curr[jsaction.Property.OWNER]) {
127 | this.usingAncestors_ = true;
128 | jsaction.domGenerator.ancestors_.reset_(
129 | curr[jsaction.Property.OWNER],
130 | /** @type {!Element} */(this.container_));
131 | }
132 | }
133 | return curr;
134 | }
135 | return null;
136 | };
137 |
138 |
139 | /**
140 | * A reusable generator for dom ancestor walks.
141 | * @private {!jsaction.domGenerator.Ancestors}
142 | */
143 | jsaction.domGenerator.ancestors_ =
144 | new jsaction.domGenerator.Ancestors();
145 |
146 |
147 | /**
148 | * A reusable generator for dom ancestor walks.
149 | * @private {!jsaction.domGenerator.EventPath}
150 | */
151 | jsaction.domGenerator.eventPath_ =
152 | new jsaction.domGenerator.EventPath();
153 |
154 |
155 | /**
156 | * Return the correct dom generator for a given event.
157 | * @param {!Event} e the event.
158 | * @param {!Element} target the events target element.
159 | * @param {!Element} container the jsaction container.
160 | * @return {!jsaction.domGenerator.Generator}
161 | */
162 | jsaction.domGenerator.getGenerator = function(e, target, container) {
163 | return e.path ? jsaction.domGenerator.eventPath_.reset_(e.path, container) :
164 | jsaction.domGenerator.ancestors_.reset_(target, container);
165 | };
166 |
--------------------------------------------------------------------------------
/generator_test.js:
--------------------------------------------------------------------------------
1 | /**
2 | */
3 | goog.provide('jsaction.GeneratorTest');
4 | goog.setTestOnly('jsaction.GeneratorTest');
5 |
6 | goog.require('goog.testing.jsunit');
7 | goog.require('jsaction.Property');
8 | goog.require('jsaction.domGenerator');
9 |
10 |
11 | function elem(id) {
12 | return document.getElementById(id);
13 | }
14 |
15 | function assertExpectedPath(g, expectedPath) {
16 | var count = 0;
17 | var i = 0;
18 | for (var n; n = g.next();) {
19 | assertEquals(expectedPath[i++], n);
20 | }
21 | }
22 |
23 | function testDomAncestorGenerator() {
24 | var g = jsaction.domGenerator.ancestors_;
25 | var target = elem('target');
26 | var container = elem('container');
27 | var expected = [
28 | elem('target'), elem('host'), elem('innercontainer'), container];
29 | g.reset_(target, container);
30 | assertExpectedPath(g, expected);
31 | }
32 |
33 |
34 | function testEventPathGenerator() {
35 | var g = jsaction.domGenerator.eventPath_;
36 | var container = elem('container');
37 | var expected = [
38 | elem('target'), elem('host'), elem('innercontainer'), container];
39 | g.reset_(expected, container);
40 | assertExpectedPath(g, expected);
41 | }
42 |
43 | function testDomAncestorGeneratorWithOwnerProperty() {
44 | var g = jsaction.domGenerator.eventPath_;
45 | var container = elem('containeractions');
46 | var actionNode = elem('actionnode');
47 | var owned = elem('owner');
48 | var element = elem('action-1');
49 | owned[jsaction.Property.OWNER] = element;
50 | var expected = [owned, element, actionNode, container];
51 | g.reset_(owned, container);
52 | assertExpectedPath(g, expected);
53 |
54 | }
55 |
56 | function testEventPathGeneratorWithOwnerProperty() {
57 | var g = jsaction.domGenerator.eventPath_;
58 | var container = elem('containeractions');
59 | var actionNode = elem('actionnode');
60 | var owned = elem('owner');
61 | var element = elem('action-1');
62 | owned[jsaction.Property.OWNER] = element;
63 | var expected = [owned, element, actionNode, container];
64 |
65 | g.reset_([owned], container);
66 | assertExpectedPath(g, expected);
67 | }
68 |
--------------------------------------------------------------------------------
/generator_test_dom.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
17 |
--------------------------------------------------------------------------------
/jsaction.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Public static API for using jsaction.
3 | */
4 |
5 | goog.provide('jsaction');
6 |
7 | goog.require('goog.asserts');
8 | goog.require('jsaction.EventType');
9 | goog.require('jsaction.dom');
10 | goog.requireType('jsaction.CustomEventDetail');
11 |
12 | /**
13 | * Create a custom event with the specified data.
14 | * @param {string} type The type of the action, e.g. 'submit'.
15 | * @param {!Object.=} opt_data An optional data payload.
16 | * @param {!Event=} opt_triggeringEvent The event that triggers this custom
17 | * event. This can be accessed from the custom event's action flow like
18 | * so: actionFlow.event().detail.triggeringEvent.
19 | * @return {!Event} The new custom event.
20 | */
21 | jsaction.createCustomEvent = function(type, opt_data, opt_triggeringEvent) {
22 | let event;
23 |
24 | // We use '_type' for the event contract, which lives in a separate
25 | // compilation unit, but also include the renamable keys so that event
26 | // consumers can access the data directly, e.g. detail.type instead of
27 | // detail['type'].
28 | const /** !jsaction.CustomEventDetail */ detail = {
29 | '_type': type,
30 | type: type,
31 | data: opt_data,
32 | triggeringEvent: opt_triggeringEvent
33 | };
34 | try {
35 | // We don't use the CustomEvent constructor directly since it isn't
36 | // supported in IE 9 or 10 and initCustomEvent below works just fine.
37 | event = document.createEvent('CustomEvent');
38 | event.initCustomEvent(jsaction.EventType.CUSTOM, true, false, detail);
39 | } catch (e) {
40 | // If custom events aren't supported, fall back to custom-named HTMLEvent.
41 | // Fallback used by Android Gingerbread, FF4-5.
42 | event = document.createEvent('HTMLEvents');
43 | event.initEvent(jsaction.EventType.CUSTOM, true, false);
44 | event['detail'] = detail;
45 | }
46 |
47 | return event;
48 | };
49 |
50 |
51 | /**
52 | * Fires a custom event with an optional payload. Only intended to be consumed
53 | * by jsaction itself. Supported in Firefox 6+, IE 9+, and all Chrome versions.
54 | *
55 | * TODO(user): Investigate polyfill options.
56 | *
57 | * @param {!Element} target The target element.
58 | * @param {string} type The type of the action, e.g. 'submit'.
59 | * @param {!Object.=} opt_data An optional data payload.
60 | * @param {!Event=} opt_triggeringEvent An optional data for the Event triggered
61 | * this custom event.
62 | */
63 | jsaction.fireCustomEvent = function(
64 | target, type, opt_data, opt_triggeringEvent) {
65 | const event = jsaction.createCustomEvent(type, opt_data, opt_triggeringEvent);
66 | target.dispatchEvent(event);
67 | };
68 |
69 |
70 | /**
71 | * Fires a custom event at descendant elements. For a given descendant of the
72 | * target element, a custom event is fired if (1) it has a jsaction handler for
73 | * the action type, and (2) the element does not have an ancestor (also a
74 | * descendant of the target element) that already handled the event.
75 | * Supported wherever fireCustomEvent is supported.
76 | *
77 | * @param {!Element} target The target element.
78 | * @param {string} type The type of the action, e.g. 'submit'. Because of an
79 | * implementation detail, type may not be 'click'.
80 | * @param {!Object.=} opt_data An optional data payload.
81 | */
82 | jsaction.broadcastCustomEvent = function(target, type, opt_data) {
83 | goog.asserts.assert(type != 'click');
84 | const matched = target.querySelectorAll(
85 | '[jsaction^="' + type + ':"], ' +
86 | '[jsaction*=";' + type + ':"], [jsaction*=" ' + type + ':"]');
87 | for (let idx = 0; idx < matched.length; ++idx) {
88 | const match = matched[idx];
89 | if (!jsaction.dom.hasAncestorInNodeList(match, matched)) {
90 | jsaction.fireCustomEvent(match, type, opt_data);
91 | }
92 | }
93 | };
94 |
--------------------------------------------------------------------------------
/jsaction_test.js:
--------------------------------------------------------------------------------
1 | goog.provide('jsaction.jsactionTest');
2 | goog.setTestOnly('jsaction.jsactionTest');
3 |
4 | goog.require('goog.events.EventType');
5 | goog.require('goog.testing.jsunit');
6 | goog.require('jsaction');
7 |
8 | var eventsToTargets = {};
9 | jsaction.fireCustomEvent = function(target, type, opt_data) {
10 | if (eventsToTargets[type]) {
11 | eventsToTargets[type].push(target);
12 | } else {
13 | eventsToTargets[type] = [target];
14 | }
15 | };
16 |
17 | // Fix Object.keys for IE8
18 | if (!Object.keys) {
19 | Object.keys = function(obj) {
20 | var keys = [];
21 |
22 | for (var i in obj) {
23 | if (obj.hasOwnProperty(i)) {
24 | keys.push(i);
25 | }
26 | }
27 |
28 | return keys;
29 | };
30 | }
31 |
32 | function setUp() {
33 | eventsToTargets = {};
34 | }
35 |
36 | function tearDown() {
37 | eventsToTargets = {};
38 | }
39 |
40 | function testBroadcastCustomEventNoMatches() {
41 | jsaction.broadcastCustomEvent(document.getElementById('no-matches'),
42 | 'customEvent');
43 | assertEquals(0, Object.keys(eventsToTargets).length);
44 | }
45 |
46 | function testBroadcastCustomEventMatches() {
47 | jsaction.broadcastCustomEvent(document.getElementById('matches'),
48 | 'customEvent');
49 | assertEquals(1, Object.keys(eventsToTargets).length);
50 | assertTrue('customEvent' in eventsToTargets);
51 | jsaction.broadcastCustomEvent(document.getElementById('matches'),
52 | 'custom2');
53 | assertEquals(2, Object.keys(eventsToTargets).length);
54 | assertTrue('custom2' in eventsToTargets);
55 | assertEquals(2, eventsToTargets['customEvent'].length);
56 | assertEquals('matches-foo', eventsToTargets['customEvent'][0].id);
57 | assertEquals('matches-bar', eventsToTargets['customEvent'][1].id);
58 |
59 | assertEquals(1, eventsToTargets['custom2'].length);
60 | assertEquals('matches2', eventsToTargets['custom2'][0].id);
61 | }
62 |
63 | function testBroadcastCustomEventNestedMatches() {
64 | jsaction.broadcastCustomEvent(document.getElementById('nested'),
65 | 'customEvent');
66 | assertEquals(1, Object.keys(eventsToTargets).length);
67 | assertTrue('customEvent' in eventsToTargets);
68 | jsaction.broadcastCustomEvent(document.getElementById('nested'),
69 | 'custom2');
70 | assertEquals(2, Object.keys(eventsToTargets).length);
71 | assertTrue('custom2' in eventsToTargets);
72 |
73 | assertEquals(3, eventsToTargets['customEvent'].length);
74 | assertEquals('nested-foo', eventsToTargets['customEvent'][0].id);
75 | assertEquals('nested-bar', eventsToTargets['customEvent'][1].id);
76 | assertEquals('nested-qux', eventsToTargets['customEvent'][2].id);
77 |
78 | assertEquals(1, eventsToTargets['custom2'].length);
79 | assertEquals('nested2', eventsToTargets['custom2'][0].id);
80 | }
81 |
82 |
83 | function testBroadcastCustomEventPartialMatchDoesNotTriggerEvent() {
84 | jsaction.broadcastCustomEvent(document.getElementById('partial_match_test'),
85 | 'partial_');
86 | assertFalse('partial_' in eventsToTargets);
87 |
88 | jsaction.broadcastCustomEvent(document.getElementById('partial_match_test'),
89 | '_match');
90 | assertFalse('_match' in eventsToTargets);
91 | }
92 |
93 |
94 | function testBroadcastCustomEventExactMatchTriggersEvent() {
95 | // Does not trigger inexact_match
96 | jsaction.broadcastCustomEvent(document.getElementById('exact_match_test'),
97 | 'exact_match');
98 | assertEquals(3, eventsToTargets['exact_match'].length);
99 | assertEquals('exact-match-a', eventsToTargets['exact_match'][0].id);
100 | assertEquals('exact-match-b', eventsToTargets['exact_match'][1].id);
101 | assertEquals('exact-match-c', eventsToTargets['exact_match'][2].id);
102 | }
103 |
--------------------------------------------------------------------------------
/jsaction_test_dom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
31 |
32 |
33 |
36 |
37 |
44 |
--------------------------------------------------------------------------------
/loader.js:
--------------------------------------------------------------------------------
1 | goog.provide('jsaction.Loader');
2 |
3 | goog.requireType('jsaction.Dispatcher');
4 | goog.requireType('jsaction.EventInfo');
5 |
6 | /**
7 | * A loader is a function that will do whatever is necessary to register
8 | * handlers for a given namespace. A loader takes a dispatcher and a namespace
9 | * as parameters.
10 | * @typedef {function(!jsaction.Dispatcher,string,?jsaction.EventInfo):void}
11 | */
12 | jsaction.Loader;
13 |
--------------------------------------------------------------------------------
/nativeevents.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All Rights Reserved.
2 |
3 | /**
4 | * @fileoverview Utility functions for generating native browser events.
5 | */
6 | goog.provide('jsaction.testing.nativeEvents');
7 | goog.setTestOnly();
8 |
9 | goog.require('goog.dom.NodeType');
10 | goog.require('goog.events.BrowserEvent');
11 | goog.require('goog.events.EventType');
12 | goog.require('goog.object');
13 | goog.require('goog.style');
14 | goog.require('goog.testing.events.Event');
15 | goog.require('jsaction.KeyCodes');
16 | goog.require('jsaction.createKeyboardEvent');
17 | goog.require('jsaction.createMouseEvent');
18 | goog.require('jsaction.createUiEvent');
19 | goog.require('jsaction.triggerEvent');
20 |
21 |
22 | /**
23 | * @typedef {{
24 | * ctrlKey:(boolean|undefined),
25 | * altKey:(boolean|undefined),
26 | * shiftKey:(boolean|undefined),
27 | * metaKey:(boolean|undefined),
28 | * }}
29 | */
30 | jsaction.testing.nativeEvents.Modifiers;
31 |
32 | /**
33 | * Simulates a blur event on the given target.
34 | * @param {!EventTarget} target The target for the event.
35 | * @return {boolean} The returnValue of the event: false if preventDefault() was
36 | * called on it, true otherwise.
37 | */
38 | jsaction.testing.nativeEvents.fireBlurEvent = function(target) {
39 | const e = new goog.testing.events.Event(goog.events.EventType.BLUR, target);
40 | return jsaction.triggerEvent(target, jsaction.createUiEvent(e));
41 | };
42 |
43 |
44 | /**
45 | * Simulates a focus event on the given target.
46 | * @param {!EventTarget} target The target for the event.
47 | * @return {boolean} The returnValue of the event: false if preventDefault() was
48 | * called on it, true otherwise.
49 | */
50 | jsaction.testing.nativeEvents.fireFocusEvent = function(target) {
51 | const e = new goog.testing.events.Event(goog.events.EventType.FOCUS, target);
52 | return jsaction.triggerEvent(target, jsaction.createUiEvent(e));
53 | };
54 |
55 |
56 | /**
57 | * Simulates a scroll event on the given target.
58 | * @param {!EventTarget} target The target for the event.
59 | * @return {boolean} The returnValue of the event: false if preventDefault() was
60 | * called on it, true otherwise.
61 | */
62 | jsaction.testing.nativeEvents.fireScrollEvent = function(target) {
63 | const e = new goog.testing.events.Event(goog.events.EventType.SCROLL, target);
64 | return jsaction.triggerEvent(target, jsaction.createUiEvent(e));
65 | };
66 |
67 |
68 | /**
69 | * Simulates a customizable event on the given target.
70 | * @param {!goog.events.EventType} eventType The type of DOM event to fire.
71 | * @param {!EventTarget} target The target for the event.
72 | * @return {boolean} The returnValue of the event: false if preventDefault() was
73 | * called on it, true otherwise.
74 | */
75 | jsaction.testing.nativeEvents.fireDomEvent = function(eventType, target) {
76 | const e = new goog.testing.events.Event(eventType, target);
77 | return jsaction.triggerEvent(target, jsaction.createUiEvent(e));
78 | };
79 |
80 |
81 | /**
82 | * Simulates a mousedown, mouseup, and then click on the given event target,
83 | * with the left mouse button.
84 | * @param {!EventTarget} target The target for the event.
85 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
86 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
87 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
88 | * event's target's position (if available), otherwise (0, 0).
89 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
90 | * BrowserEvent.
91 | * @return {boolean} The returnValue of the sequence: false if preventDefault()
92 | * was called on any of the events, true otherwise.
93 | */
94 | jsaction.testing.nativeEvents.fireClickSequence =
95 | function(target, opt_button, opt_coords, opt_eventProperties) {
96 | return !!(
97 | jsaction.testing.nativeEvents.fireMouseDownEvent(
98 | target, opt_button, opt_coords, opt_eventProperties) &
99 | jsaction.testing.nativeEvents.fireMouseUpEvent(
100 | target, opt_button, opt_coords, opt_eventProperties) &
101 | jsaction.testing.nativeEvents.fireClickEvent(
102 | target, opt_button, opt_coords, opt_eventProperties));
103 | };
104 |
105 |
106 | /**
107 | * Simulates a mousedown event on the given target.
108 | * @param {!EventTarget} target The target for the event.
109 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
110 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
111 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
112 | * event's target's position (if available), otherwise (0, 0).
113 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
114 | * BrowserEvent.
115 | * @return {boolean} false if preventDefault() was called, true otherwise.
116 | */
117 | jsaction.testing.nativeEvents.fireMouseDownEvent = function(
118 | target, opt_button, opt_coords, opt_eventProperties) {
119 | return jsaction.testing.nativeEvents.fireMouseButtonEvent_(
120 | goog.events.EventType.MOUSEDOWN, target, opt_button, opt_coords,
121 | opt_eventProperties);
122 | };
123 |
124 |
125 |
126 | /**
127 | * Simulates a mouseup event on the given target.
128 | * @param {!EventTarget} target The target for the event.
129 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
130 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
131 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
132 | * event's target's position (if available), otherwise (0, 0).
133 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
134 | * BrowserEvent.
135 | * @return {boolean} false if preventDefault() was called, true otherwise.
136 | */
137 | jsaction.testing.nativeEvents.fireMouseUpEvent = function(
138 | target, opt_button, opt_coords, opt_eventProperties) {
139 | return jsaction.testing.nativeEvents.fireMouseButtonEvent_(
140 | goog.events.EventType.MOUSEUP, target, opt_button, opt_coords,
141 | opt_eventProperties);
142 | };
143 |
144 |
145 | /**
146 | * Simulates a click event on the given target.
147 | * @param {!EventTarget} target The target for the event.
148 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
149 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
150 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
151 | * event's target's position (if available), otherwise (0, 0).
152 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
153 | * BrowserEvent.
154 | * @return {boolean} false if preventDefault() was called, true otherwise.
155 | */
156 | jsaction.testing.nativeEvents.fireClickEvent =
157 | function(target, opt_button, opt_coords, opt_eventProperties) {
158 | return jsaction.testing.nativeEvents.fireMouseButtonEvent_(
159 | goog.events.EventType.CLICK, target, opt_button, opt_coords,
160 | opt_eventProperties);
161 | };
162 |
163 |
164 | /**
165 | * Simulates a mouseover event on the given target.
166 | * @param {!EventTarget} target The target for the event.
167 | * @return {boolean} false if preventDefault() was called, true otherwise.
168 | */
169 | jsaction.testing.nativeEvents.fireMouseOverEvent = function(target) {
170 | return jsaction.testing.nativeEvents.fireMouseButtonEvent_(
171 | goog.events.EventType.MOUSEOVER, target);
172 | };
173 |
174 |
175 | /**
176 | * Simulates a mouseout event on the given target.
177 | * @param {!EventTarget} target The target for the event.
178 | * @return {boolean} false if preventDefault() was called, true otherwise.
179 | */
180 | jsaction.testing.nativeEvents.fireMouseOutEvent = function(target) {
181 | return jsaction.testing.nativeEvents.fireMouseButtonEvent_(
182 | goog.events.EventType.MOUSEOUT, target);
183 | };
184 |
185 |
186 | /**
187 | * Simulates a mousemove event on the given target.
188 | * @param {!EventTarget} target The target for the event.
189 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
190 | * event's target's position (if available), otherwise (0, 0).
191 | * @return {boolean} The returnValue of the event: false if preventDefault() was
192 | * called on it, true otherwise.
193 | */
194 | jsaction.testing.nativeEvents.fireMouseMoveEvent = function(
195 | target, opt_coords) {
196 | const e = new goog.testing.events.Event(
197 | goog.events.EventType.MOUSEMOVE, target);
198 | jsaction.testing.nativeEvents.setEventClientXY_(e, opt_coords);
199 | return jsaction.triggerEvent(target, jsaction.createMouseEvent(e));
200 | };
201 |
202 |
203 | /**
204 | * Creates a mouse button event.
205 | * @param {string} type The event type.
206 | * @param {!EventTarget} target The target for the event.
207 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
208 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
209 | * @param {boolean=} opt_modifierKey Create the event with the modifier key
210 | * registered as down.
211 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
212 | * event's target's position (if available), otherwise (0, 0).
213 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
214 | * BrowserEvent.
215 | * @return {!Event} The created event.
216 | */
217 | jsaction.testing.nativeEvents.createMouseButtonEvent = function(
218 | type, target, opt_button, opt_modifierKey, opt_coords,
219 | opt_eventProperties) {
220 | const e = new goog.testing.events.Event(type, target);
221 | e.button = opt_button || goog.events.BrowserEvent.MouseButton.LEFT;
222 | jsaction.testing.nativeEvents.setEventClientXY_(e, opt_coords);
223 | if (opt_eventProperties) {
224 | goog.object.extend(e, opt_eventProperties);
225 | }
226 | if (opt_modifierKey) {
227 | e.ctrlKey = true;
228 | e.metaKey = true;
229 | }
230 | return jsaction.createMouseEvent(e);
231 | };
232 |
233 |
234 | /**
235 | * A static helper function that sets the mouse position to the event.
236 | * @param {!Event} event A simulated native event.
237 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
238 | * event's target's position (if available), otherwise (0, 0).
239 | * @private
240 | */
241 | jsaction.testing.nativeEvents.setEventClientXY_ = function(event, opt_coords) {
242 | if (!opt_coords && event.target &&
243 | event.target.nodeType == goog.dom.NodeType.ELEMENT) {
244 | try {
245 | opt_coords =
246 | goog.style.getClientPosition(/** @type {!Element} **/ (event.target));
247 | } catch (ex) {
248 | // IE sometimes throws if it can't get the position.
249 | }
250 | }
251 | event.clientX = opt_coords ? opt_coords.x : 0;
252 | event.clientY = opt_coords ? opt_coords.y : 0;
253 |
254 | // Pretend the browser window is at (0, 0).
255 | event.screenX = event.clientX;
256 | event.screenY = event.clientY;
257 | };
258 |
259 |
260 | /**
261 | * Helper function to fire a mouse event with a mouse button. IE < 9 only allows
262 | * firing events using the left mouse button.
263 | * @param {string} type The event type.
264 | * @param {!EventTarget} target The target for the event.
265 | * @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
266 | * defaults to `goog.events.BrowserEvent.MouseButton.LEFT`.
267 | * @param {?goog.math.Coordinate=} opt_coords Mouse position. Defaults to
268 | * event's target's position (if available), otherwise (0, 0).
269 | * @param {?Object=} opt_eventProperties Event properties to be mixed into the
270 | * BrowserEvent.
271 | * @return {boolean} The value returned by the browser event,
272 | * which returns false iff 'preventDefault' was invoked.
273 | * @private
274 | */
275 | jsaction.testing.nativeEvents.fireMouseButtonEvent_ = function(
276 | type, target, opt_button, opt_coords, opt_eventProperties) {
277 | const e = jsaction.testing.nativeEvents.createMouseButtonEvent(
278 | type, target, opt_button, undefined, opt_coords, opt_eventProperties);
279 | return jsaction.triggerEvent(target, e);
280 | };
281 |
282 |
283 | /**
284 | * Creates and initializes a key event.
285 | * @param {string} eventType The type of event to create ("keydown", "keyup",
286 | * or "keypress").
287 | * @param {!EventTarget} target The event target.
288 | * @param {number} keyCode The key code.
289 | * @param {number} charCode The character code produced by the key.
290 | * @param {!jsaction.testing.nativeEvents.Modifiers=} modifiers
291 | * @return {boolean} The value returned by the browser event,
292 | * which returns false iff 'preventDefault' was invoked.
293 | */
294 | jsaction.testing.nativeEvents.fireKeyEvent = function(
295 | eventType, target, keyCode, charCode, modifiers = {}) {
296 | const e = new goog.testing.events.Event(eventType, target);
297 | e.charCode = charCode;
298 | e.keyCode = keyCode;
299 | goog.object.extend(e, modifiers);
300 | return jsaction.triggerEvent(target, jsaction.createKeyboardEvent(e));
301 | };
302 |
303 |
304 | /**
305 | * Generates a series of events simulating a key press on the given element.
306 | * @param {!EventTarget} target The event target.
307 | * @param {number} keyCode The key code.
308 | * @param {number=} charCode The character code produced by the key.
309 | * @param {!jsaction.testing.nativeEvents.Modifiers=} modifiers
310 | * @return {boolean} False if preventDefault() was called by any of the handlers
311 | */
312 | jsaction.testing.nativeEvents.simulateKeyPress = function(
313 | target, keyCode, charCode = keyCode, modifiers = {}) {
314 | let e;
315 | e = jsaction.testing.nativeEvents.fireKeyEvent(
316 | goog.events.EventType.KEYDOWN, target, keyCode, charCode, modifiers);
317 | e = jsaction.testing.nativeEvents.fireKeyEvent(
318 | goog.events.EventType.KEYPRESS, target, keyCode, charCode,
319 | modifiers) &&
320 | e;
321 |
322 | // A click event is fired by browsers when pressing Enter on
323 | // elements, for a11y.
324 | if ((keyCode == jsaction.KeyCodes.ENTER ||
325 | keyCode == jsaction.KeyCodes.MAC_ENTER) &&
326 | target.nodeName == 'BUTTON') {
327 | e = jsaction.testing.nativeEvents.fireClickEvent(
328 | target, undefined, undefined, modifiers) &&
329 | e;
330 | }
331 |
332 | e = jsaction.testing.nativeEvents.fireKeyEvent(
333 | goog.events.EventType.KEYUP, target, keyCode, charCode, modifiers) &&
334 | e;
335 | // Same as the Enter "click", but for Space it happens after keyup.
336 | if ((keyCode == jsaction.KeyCodes.SPACE) && target.nodeName == 'BUTTON') {
337 | e = jsaction.testing.nativeEvents.fireClickEvent(
338 | target, undefined, undefined, modifiers) &&
339 | e;
340 | }
341 | return e;
342 | };
343 |
--------------------------------------------------------------------------------
/replay.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All rights reserved.
2 |
3 | /**
4 | *
5 | * @fileoverview Functions for replaying events by the jsaction
6 | * Dispatcher.
7 | */
8 | goog.provide('jsaction.createKeyboardEvent');
9 | goog.provide('jsaction.createMouseEvent');
10 | goog.provide('jsaction.createUiEvent');
11 | goog.provide('jsaction.replayEvent');
12 | goog.provide('jsaction.triggerEvent');
13 |
14 | goog.require('goog.asserts');
15 | goog.require('goog.functions');
16 | goog.require('jsaction');
17 | goog.require('jsaction.EventType');
18 | goog.require('jsaction.event');
19 |
20 |
21 | /**
22 | * Replays an event.
23 | * @param {!jsaction.EventInfo} eventInfo The event info record.
24 | */
25 | jsaction.replayEvent = function(eventInfo) {
26 | const event = jsaction.createEvent(eventInfo['event'], eventInfo['eventType']);
27 | jsaction.triggerEvent(eventInfo['targetElement'], event);
28 | };
29 |
30 |
31 | /**
32 | * Checks if a given event was triggered by the keyboard.
33 | * @param {string} eventType The event type.
34 | * @return {boolean} Whether it's a keyboard event.
35 | * @private
36 | */
37 | jsaction.isKeyboardEvent_ = function(eventType) {
38 | return eventType == jsaction.EventType.KEYPRESS ||
39 | eventType == jsaction.EventType.KEYDOWN ||
40 | eventType == jsaction.EventType.KEYUP;
41 | };
42 |
43 |
44 | /**
45 | * Checks if a given event was triggered by the mouse.
46 | * @param {string} eventType The event type.
47 | * @return {boolean} Whether it's a mouse event.
48 | * @private
49 | */
50 | jsaction.isMouseEvent_ = function(eventType) {
51 | // TODO(ruilopes): Verify if Drag events should be bound here.
52 | return eventType == jsaction.EventType.CLICK ||
53 | eventType == jsaction.EventType.DBLCLICK ||
54 | eventType == jsaction.EventType.MOUSEDOWN ||
55 | eventType == jsaction.EventType.MOUSEOVER ||
56 | eventType == jsaction.EventType.MOUSEOUT ||
57 | eventType == jsaction.EventType.MOUSEMOVE;
58 | };
59 |
60 |
61 | /**
62 | * Checks if a given event is a general UI event.
63 | * @param {string} eventType The event type.
64 | * @return {boolean} Whether it's a focus event.
65 | * @private
66 | */
67 | jsaction.isUiEvent_ = function(eventType) {
68 | // Almost nobody supports the W3C method of creating FocusEvents.
69 | // For now, we're going to use the UIEvent as a super-interface.
70 | return eventType == jsaction.EventType.FOCUS ||
71 | eventType == jsaction.EventType.BLUR ||
72 | eventType == jsaction.EventType.FOCUSIN ||
73 | eventType == jsaction.EventType.FOCUSOUT ||
74 | eventType == jsaction.EventType.SCROLL;
75 | };
76 |
77 |
78 | /**
79 | * Create a whitespace-delineated list of modifier keys that should be
80 | * considered to be active on the event's key. See details at
81 | * https://developer.mozilla.org/en/DOM/KeyboardEvent.
82 | * @param {boolean} alt Alt pressed.
83 | * @param {boolean} ctrl Control pressed.
84 | * @param {boolean} meta Command pressed (OSX only).
85 | * @param {boolean} shift Shift pressed.
86 | * @return {string} The constructed modifier keys string.
87 | * @private
88 | */
89 | jsaction.createKeyboardModifiersList_ = function(alt, ctrl, meta, shift) {
90 | const keys = [];
91 | if (alt) {
92 | keys.push('Alt');
93 | }
94 | if (ctrl) {
95 | keys.push('Control');
96 | }
97 | if (meta) {
98 | keys.push('Meta');
99 | }
100 | if (shift) {
101 | keys.push('Shift');
102 | }
103 | return keys.join(' ');
104 | };
105 |
106 |
107 | /**
108 | * Creates a UI event object for replaying through the DOM.
109 | * @param {!Event} original The event to create a new event from.
110 | * @param {string=} opt_eventType The type this event is being handled as by
111 | * jsaction. E.g. blur events are handled as focusout
112 | * @return {!Event} The event object.
113 | */
114 | jsaction.createUiEvent = function(original, opt_eventType) {
115 | let event;
116 | if (document.createEvent) {
117 | // Event creation as per W3C event model specification. This codepath
118 | // is used by most non-IE browsers and also by IE 9 and later.
119 | event = document.createEvent('UIEvent');
120 | // On IE and Opera < 12, we must provide non-undefined values to
121 | // initEvent, otherwise it will fail.
122 | event.initUIEvent(
123 | opt_eventType || original.type,
124 | original.bubbles !== undefined ? original.bubbles : true,
125 | original.cancelable || false, original.view || window,
126 | original.detail || 0 // detail
127 | );
128 | } else {
129 | goog.asserts.assert(document.createEventObject);
130 | // Older versions of IE (up to version 8) do not support the
131 | // W3C event model. Use the IE specific function instead.
132 | event = document.createEventObject();
133 | event.type = opt_eventType || original.type;
134 | event.bubbles = (original.bubbles !== undefined) ? original.bubbles : true;
135 | event.cancelable = original.cancelable || false;
136 | event.view = original.view || window;
137 | event.detail = original.detail || 0;
138 | }
139 | // Some focus events also have a nullable relatedTarget value which isn't
140 | // directly supported in the initUIEvent() method.
141 | event.relatedTarget = original.relatedTarget || null;
142 | event.originalTimestamp = original.timeStamp;
143 | return event;
144 | };
145 |
146 | /**
147 | * Creates a keyboard event object for replaying through the DOM.
148 | * @param {!Event} original The event to create a new event from.
149 | * @param {string=} opt_eventType The type this event is being handled as by
150 | * jsaction. E.g. a keypress is handled as click in some cases.
151 | * @return {!Event} The event object.
152 | */
153 | jsaction.createKeyboardEvent = function(original, opt_eventType) {
154 | let event;
155 | if (jsaction.event.isSafari) {
156 | // We have to fall back to a generic event for Safari, which has the WebKit
157 | // keyCode bug noted below, but is also incapable of fixing it with
158 | // Object.defineProperty due to another bug:
159 | // https://bugs.webkit.org/show_bug.cgi?id=36423
160 | event = jsaction.createGenericEvent_(original, opt_eventType);
161 | event.ctrlKey = original.ctrlKey;
162 | event.altKey = original.altKey;
163 | event.shiftKey = original.shiftKey;
164 | event.metaKey = original.metaKey;
165 | event.keyCode = original.keyCode;
166 | event.charCode = original.charCode;
167 | event.originalTimestamp = original.timeStamp;
168 | return event;
169 | }
170 | if (document.createEvent) {
171 | // Event creation as per W3C event model specification. This codepath
172 | // is used by most non-IE browsers and also by IE 9 and later.
173 | event = document.createEvent('KeyboardEvent');
174 | if (event.initKeyboardEvent) {
175 | // W3C DOM Level 3 Events model.
176 | const modifiers = jsaction.createKeyboardModifiersList_(original.altKey,
177 | original.ctrlKey, original.metaKey, original.shiftKey);
178 | event.initKeyboardEvent(
179 | opt_eventType || original.type,
180 | true,
181 | true,
182 | window,
183 | original.charCode,
184 | original.keyCode,
185 | original.location,
186 | modifiers,
187 | original.repeat,
188 | original.locale);
189 |
190 | // Blink and Webkit have a long-standing bug that causes the 'keyCode' and
191 | // 'which' properties to always be set to 0 when synthesizing a keyboard
192 | // event. Details at: https://bugs.webkit.org/show_bug.cgi?id=16735
193 | // It unfortunately looks like IE9+ also copied this behavior, when they
194 | // implemented DOM3 events. We work around it by redefining the noted
195 | // properties; a simple assignment here would fail because the native
196 | // properties are readonly.
197 | if (jsaction.event.isWebKit || jsaction.event.isIe ||
198 | jsaction.event.isGecko) {
199 | const keyCodeGetter = goog.functions.constant(original.keyCode);
200 | Object.defineProperty(event, 'keyCode', {
201 | get: keyCodeGetter
202 | });
203 | Object.defineProperty(event, 'which', {
204 | get: keyCodeGetter
205 | });
206 | }
207 |
208 | } else {
209 | // Gecko only supports an older/deprecated version from DOM Level 2. See
210 | // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyEvent
211 | // for details.
212 | goog.asserts.assert(event.initKeyEvent);
213 | event.initKeyEvent(
214 | opt_eventType || original.type,
215 | true,
216 | true,
217 | window,
218 | original.ctrlKey,
219 | original.altKey,
220 | original.shiftKey,
221 | original.metaKey,
222 | original.keyCode,
223 | original.charCode);
224 | }
225 | } else {
226 | // Older versions of IE (up to version 8) do not support the
227 | // W3C event model. Use the IE specific function instead.
228 | goog.asserts.assert(document.createEventObject);
229 | event = document.createEventObject();
230 | event.type = opt_eventType || original.type;
231 | event.repeat = original.repeat;
232 | event.ctrlKey = original.ctrlKey;
233 | event.altKey = original.altKey;
234 | event.shiftKey = original.shiftKey;
235 | event.metaKey = original.metaKey;
236 | event.keyCode = original.keyCode;
237 | event.charCode = original.charCode;
238 | }
239 | event.originalTimestamp = original.timeStamp;
240 | return event;
241 | };
242 |
243 |
244 | /**
245 | * Creates a mouse event object for replaying through the DOM.
246 | * @param {!Event} original The event to create a new event from.
247 | * @param {string=} opt_eventType The type this event is being handled as by
248 | * jsaction. E.g. a keypress is handled as click in some cases.
249 | * @return {!MouseEvent} The event object.
250 | */
251 | jsaction.createMouseEvent = function(original, opt_eventType) {
252 | let event;
253 | if (document.createEvent) {
254 | // Event creation as per W3C event model specification. This codepath
255 | // is used by most non-IE browsers and also by IE 9 and later.
256 | event = document.createEvent('MouseEvent');
257 | // On IE and Opera < 12, we must provide non-undefined values to
258 | // initMouseEvent, otherwise it will fail.
259 | event.initMouseEvent(
260 | opt_eventType || original.type,
261 | true, // canBubble
262 | true, // cancelable
263 | window,
264 | original.detail || 1,
265 | original.screenX || 0,
266 | original.screenY || 0,
267 | original.clientX || 0,
268 | original.clientY || 0,
269 | original.ctrlKey || false,
270 | original.altKey || false,
271 | original.shiftKey || false,
272 | original.metaKey || false,
273 | original.button || 0,
274 | original.relatedTarget || null);
275 |
276 | } else {
277 | goog.asserts.assert(document.createEventObject);
278 | // Older versions of IE (up to version 8) do not support the
279 | // W3C event model. Use the IE specific function instead.
280 | event = document.createEventObject();
281 | event.type = opt_eventType || original.type;
282 | event.clientX = original.clientX;
283 | event.clientY = original.clientY;
284 | event.button = original.button;
285 | event.detail = original.detail;
286 | event.ctrlKey = original.ctrlKey;
287 | event.altKey = original.altKey;
288 | event.shiftKey = original.shiftKey;
289 | event.metaKey = original.metaKey;
290 | }
291 | event.originalTimestamp = original.timeStamp;
292 | return event;
293 | };
294 |
295 |
296 | /**
297 | * Creates a generic event object for replaying through the DOM.
298 | * @param {!Event} original The event to create a new event from.
299 | * @param {string=} opt_eventType The type this event is being handled as by
300 | * jsaction. E.g. a keypress is handled as click in some cases.
301 | * @return {!Event} The event object.
302 | * @private
303 | */
304 | jsaction.createGenericEvent_ = function(original, opt_eventType) {
305 | let event;
306 | if (document.createEvent) {
307 | // Event creation as per W3C event model specification. This codepath
308 | // is used by most non-IE browsers and also by IE 9 and later.
309 | event = document.createEvent('Event');
310 | event.initEvent(
311 | opt_eventType || original.type,
312 | true,
313 | true);
314 | } else {
315 | // Older versions of IE (up to version 8) do not support the
316 | // W3C event model. Use the IE specific function instead.
317 | goog.asserts.assert(document.createEventObject);
318 | event = document.createEventObject();
319 | event.type = opt_eventType || original.type;
320 | }
321 | event.originalTimestamp = original.timeStamp;
322 | return event;
323 | };
324 |
325 |
326 | /**
327 | * Creates an event object for replaying through the DOM.
328 | * NOTE(ruilopes): This function is visible just for testing. Please don't use
329 | * it outside JsAction internal testing.
330 | * TODO(ruilopes): Add support for FocusEvent and WheelEvent.
331 | * @param {!Event} original The event to create a new event from.
332 | * @param {string=} opt_eventType The type this event is being handled as by
333 | * jsaction. E.g. a keypress is handled as click in some cases.
334 | * @return {!Event} The event object.
335 | */
336 | jsaction.createEvent = function(original, opt_eventType) {
337 | let event;
338 | let eventType;
339 | if (original.type == jsaction.EventType.CUSTOM) {
340 | eventType = jsaction.EventType.CUSTOM;
341 | } else {
342 | eventType = opt_eventType || original.type;
343 | }
344 |
345 | if (jsaction.isKeyboardEvent_(eventType)) {
346 | event = jsaction.createKeyboardEvent(original, opt_eventType);
347 | } else if (jsaction.isMouseEvent_(eventType)) {
348 | event = jsaction.createMouseEvent(original, opt_eventType);
349 | } else if (jsaction.isUiEvent_(eventType)) {
350 | event = jsaction.createUiEvent(original, opt_eventType);
351 | } else if (eventType == jsaction.EventType.CUSTOM) {
352 | goog.asserts.assert(opt_eventType);
353 | event = jsaction.createCustomEvent(
354 | opt_eventType,
355 | original['detail']['data'],
356 | original['detail']['triggeringEvent']);
357 | event.originalTimestamp = original.timeStamp;
358 | } else {
359 | // This ensures we don't send an undefined event object to the replayer.
360 | event = jsaction.createGenericEvent_(original, opt_eventType);
361 | }
362 | return event;
363 | };
364 |
365 |
366 | /**
367 | * Sends an event for replay to the DOM.
368 | * @param {!EventTarget} target The target for the event.
369 | * @param {!Event} event The event object.
370 | * @return {boolean} The return value of the event replay, i.e., whether
371 | * preventDefault() was called on it.
372 | */
373 | jsaction.triggerEvent = function(target, event) {
374 | if (target.dispatchEvent) {
375 | return target.dispatchEvent(event);
376 | } else {
377 | goog.asserts.assert(target.fireEvent);
378 | return target.fireEvent('on' + event.type, event);
379 | }
380 | };
381 |
--------------------------------------------------------------------------------
/replay_test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc. All Rights Reserved.
2 | // Author: ruilopes@google.com (Rui do Nascimento Dias Lopes)
3 |
4 | /** @suppress {extraProvide} */
5 | goog.provide('jsaction.replayEventTest');
6 | goog.setTestOnly('jsaction.replayEventTest');
7 |
8 | goog.require('goog.testing.jsunit');
9 | goog.require('jsaction.EventType');
10 | goog.require('jsaction.replayEvent');
11 |
12 |
13 | var mockEvent = {
14 | type: 'click',
15 | detail: 1,
16 | screenX: 0,
17 | screenY: 0,
18 | clientX: 0,
19 | clientY: 0,
20 | ctrlKey: false,
21 | altKey: false,
22 | shiftKey: false,
23 | metaKey: false,
24 | button: 0,
25 | relatedTarget: null,
26 | timeStamp: 1234
27 | };
28 |
29 |
30 | function createEventArrayForTypes(eventTypes) {
31 | var events = [];
32 | for (var i = 0; i < eventTypes.length; ++i) {
33 | events.push({'type': eventTypes[i]});
34 | }
35 | return events;
36 | }
37 |
38 |
39 | function testIsUiEvent() {
40 | var uiEventTypes = [
41 | jsaction.EventType.BLUR,
42 | jsaction.EventType.FOCUS,
43 | jsaction.EventType.FOCUSIN,
44 | jsaction.EventType.FOCUSOUT,
45 | jsaction.EventType.SCROLL
46 | ];
47 | var uiEvents = createEventArrayForTypes(uiEventTypes);
48 | for (var i = 0; i < uiEvents.length; ++i) {
49 | assertTrue(jsaction.isUiEvent_(uiEvents[i].type));
50 | }
51 | assertFalse(jsaction.isUiEvent_(
52 | {'type': jsaction.EventType.KEYUP}));
53 | }
54 |
55 |
56 | function testIsKeyboardEvent() {
57 | var keyboardEventTypes = [
58 | jsaction.EventType.KEYPRESS,
59 | jsaction.EventType.KEYDOWN,
60 | jsaction.EventType.KEYUP
61 | ];
62 | var keyboardEvents = createEventArrayForTypes(keyboardEventTypes);
63 | for (var i = 0; i < keyboardEvents.length; ++i) {
64 | assertTrue(jsaction.isKeyboardEvent_(keyboardEvents[i].type));
65 | }
66 | assertFalse(jsaction.isKeyboardEvent_(jsaction.EventType.MOUSEDOWN));
67 | }
68 |
69 |
70 | function testIsMouseEvent() {
71 | var mouseEventTypes = [
72 | jsaction.EventType.CLICK,
73 | jsaction.EventType.DBLCLICK,
74 | jsaction.EventType.MOUSEDOWN,
75 | jsaction.EventType.MOUSEOVER,
76 | jsaction.EventType.MOUSEOUT,
77 | jsaction.EventType.MOUSEMOVE
78 | ];
79 | var mouseEvents = createEventArrayForTypes(mouseEventTypes);
80 | for (var i = 0; i < mouseEvents.length; ++i) {
81 | assertTrue(jsaction.isMouseEvent_(mouseEvents[i].type));
82 | }
83 | assertFalse(jsaction.isMouseEvent_(
84 | {'type': jsaction.EventType.KEYUP}));
85 | }
86 |
87 |
88 | function testCreateUiEvent() {
89 | var event = {
90 | 'type': jsaction.EventType.BLUR,
91 | 'bubbles': false,
92 | 'cancelable': false,
93 | 'view' : window,
94 | 'detail': 0,
95 | 'relatedTarget': null,
96 | 'timeStamp': 1234
97 | };
98 | var nativeEvent = jsaction.createUiEvent(event);
99 | assertEquals(event.type, nativeEvent.type);
100 | assertEquals(event.bubbles, nativeEvent.bubbles);
101 | assertEquals(event.cancelable, nativeEvent.cancelable);
102 | assertEquals(event.view, nativeEvent.view);
103 | assertEquals(event.detail, nativeEvent.detail);
104 | assertEquals(event.relatedTarget, nativeEvent.relatedTarget);
105 | assertEquals(event.timeStamp, nativeEvent.originalTimestamp);
106 | }
107 |
108 |
109 | function testCreateKeyboardModifiersList() {
110 | assertEquals('Alt Control Meta Shift',
111 | jsaction.createKeyboardModifiersList_(true, true, true, true));
112 | assertEquals('',
113 | jsaction.createKeyboardModifiersList_(false, false, false, false));
114 | assertEquals('Alt',
115 | jsaction.createKeyboardModifiersList_(true, false, false, false));
116 | assertEquals('Meta',
117 | jsaction.createKeyboardModifiersList_(false, false, true, false));
118 | }
119 |
120 |
121 | function testCreateKeyboardEvent() {
122 | var event = {
123 | 'type': jsaction.EventType.KEYPRESS,
124 | 'charCode': 13,
125 | 'keyCode': 13,
126 | 'location': 0,
127 | 'modifiers': '',
128 | 'repeat': false,
129 | 'locale': '',
130 | 'ctrlKey': false,
131 | 'altKey': false,
132 | 'shiftKey': false,
133 | 'metaKey': false,
134 | 'timeStamp': 1234
135 | };
136 | var nativeEvent = jsaction.createKeyboardEvent(event);
137 | assertEquals(event.keyCode, nativeEvent.keyCode);
138 | assertEquals(event.type, nativeEvent.type);
139 | assertEquals(event.timeStamp, nativeEvent.originalTimestamp);
140 | }
141 |
142 |
143 | function testCreateMouseEvent() {
144 | var event = {
145 | 'type': jsaction.EventType.MOUSEDOWN,
146 | 'detail': 0,
147 | 'screenX': 0,
148 | 'screenY': 0,
149 | 'clientX': 0,
150 | 'clientY': 0,
151 | 'ctrlKey': false,
152 | 'altKey': false,
153 | 'shiftKey': false,
154 | 'metaKey': false,
155 | 'button': 0,
156 | 'relatedTarget': null,
157 | 'timeStamp': 1234
158 | };
159 | var nativeEvent = jsaction.createMouseEvent(event);
160 | assertEquals(event.type, nativeEvent.type);
161 | assertEquals(event.timeStamp, nativeEvent.originalTimestamp);
162 | }
163 |
164 |
165 | function testCreateGenericEvent() {
166 | var event = {
167 | 'type': jsaction.EventType.UNLOAD,
168 | 'timeStamp': 1234
169 | };
170 | var nativeEvent = jsaction.createGenericEvent_(event);
171 | assertEquals(event.type, nativeEvent.type);
172 | assertEquals(event.timeStamp, nativeEvent.originalTimestamp);
173 | }
174 |
175 |
176 | function testCreateEvent() {
177 | var event = {
178 | 'type': jsaction.EventType.MOUSEDOWN,
179 | 'detail': 0,
180 | 'screenX': 0,
181 | 'screenY': 0,
182 | 'clientX': 0,
183 | 'clientY': 0,
184 | 'ctrlKey': false,
185 | 'altKey': false,
186 | 'shiftKey': false,
187 | 'metaKey': false,
188 | 'button': 0,
189 | 'relatedTarget': null,
190 | 'timeStamp': 1234
191 | };
192 | var nativeEvent = jsaction.createEvent(event);
193 | assertEquals(event.type, nativeEvent.type);
194 | assertEquals(event.timeStamp, nativeEvent.originalTimestamp);
195 | }
196 |
197 |
198 | function testTriggerEventWithDispatchEvent() {
199 | var dispatchEventCalled = false;
200 | var eventPassed = {'type': 'FOOBAR'};
201 | var elem = {};
202 | elem.dispatchEvent = function(event) {
203 | dispatchEventCalled = true;
204 | assertEquals(eventPassed, event);
205 | return false;
206 | };
207 | assertFalse(jsaction.triggerEvent(elem, eventPassed));
208 | assertTrue(dispatchEventCalled);
209 | }
210 |
211 |
212 | function testTriggerEventWithFireEvent() {
213 | var fireEventCalled = false;
214 | var eventPassed = {'type': 'FOOBAR'};
215 | var elem = {};
216 | elem.fireEvent = function(eventType, event) {
217 | fireEventCalled = true;
218 | assertEquals('onFOOBAR', eventType);
219 | assertEquals(eventPassed, event);
220 | return false;
221 | };
222 | assertFalse(jsaction.triggerEvent(elem, eventPassed));
223 | assertTrue(fireEventCalled);
224 | }
225 |
226 |
227 | function testReplayEvent() {
228 | var onclickCalled = false;
229 | document.body.onclick = function() {
230 | onclickCalled = true;
231 | };
232 | var event = jsaction.createEvent(mockEvent);
233 | eventInfo = {
234 | 'event': event,
235 | 'targetElement': document.body
236 | };
237 | jsaction.replayEvent(eventInfo);
238 | assertTrue(onclickCalled);
239 | document.body.onclick = null;
240 | }
241 |
--------------------------------------------------------------------------------
/syntax.js:
--------------------------------------------------------------------------------
1 | // Copyright 2011 Google Inc. All rights reserved.
2 |
3 | /**
4 | *
5 | * @fileoverview Defines and documents the various lexical components
6 | * that make up the different aspects of the jsaction syntax.
7 | *
8 | * NOTE(user): Yep, the things defined here are lexical, but their
9 | * grouping and documentation is what defines the jsaction syntax.
10 | */
11 |
12 | goog.provide('jsaction.A11y');
13 | goog.provide('jsaction.Attribute');
14 | goog.provide('jsaction.Branch');
15 | goog.provide('jsaction.Char');
16 | goog.provide('jsaction.EventType');
17 | goog.provide('jsaction.KeyCodes');
18 | goog.provide('jsaction.Name');
19 | goog.provide('jsaction.Property');
20 | goog.provide('jsaction.TagName');
21 | goog.provide('jsaction.Tick');
22 | goog.provide('jsaction.UrlParam');
23 |
24 |
25 | /**
26 | * Defines special EventInfo and Event properties used when
27 | * A11Y_SUPPORT_IN_DISPATCHER is enabled.
28 | * @const
29 | */
30 | jsaction.A11y = {
31 | /**
32 | * An event-type set when the event contract detects a KEYDOWN event but
33 | * doesn't know if the key press can be treated like a click. The dispatcher
34 | * will use this event-type to parse the keypress and handle it accordingly.
35 | */
36 | MAYBE_CLICK_EVENT_TYPE: 'maybe_click',
37 |
38 | /**
39 | * A property added to a dispatched event that had the MAYBE_CLICK_EVENTTYPE
40 | * event-type but could not be used as a click. The dispatcher sets this
41 | * property for non-global dispatches before it retriggers the event and it
42 | * signifies that the event contract should not dispatch this event globally.
43 | */
44 | SKIP_GLOBAL_DISPATCH: 'a11ysgd',
45 |
46 | /**
47 | * A property added to a dispatched event that had the MAYBE_CLICK_EVENTTYPE
48 | * event-type but could not be used as a click. The dispatcher sets this
49 | * property before it retriggers the event and it signifies that the event
50 | * contract should not look at CLICK actions for KEYDOWN events.
51 | */
52 | SKIP_A11Y_CHECK: 'a11ysc',
53 | };
54 |
55 |
56 | /**
57 | * All attributes used by jsaction.
58 | * @enum {string}
59 | */
60 | jsaction.Attribute = {
61 | /**
62 | * The jsaction attribute defines a mapping of a DOM event to a
63 | * generic event (aka jsaction), to which the actual event handlers
64 | * that implement the behavior of the application are bound. The
65 | * value is a semicolon separated list of colon separated pairs of
66 | * an optional DOM event name and a jsaction name. If the optional
67 | * DOM event name is omitted, 'click' is assumed. The jsaction names
68 | * are dot separated pairs of a namespace and a simple jsaction
69 | * name. If the namespace is absent, it is taken from the closest
70 | * ancestor element with a jsnamespace attribute, if there is
71 | * any. If there is no ancestor with a jsnamespace attribute, the
72 | * simple name is assumed to be the jsaction name.
73 | *
74 | * Used by EventContract.
75 | */
76 | JSACTION: 'jsaction',
77 |
78 | /**
79 | * The jsnamespace attribute provides the namespace part of the
80 | * jaction names occurring in the jsaction attribute where it's
81 | * missing.
82 | *
83 | * Used by EventContract.
84 | */
85 | JSNAMESPACE: 'jsnamespace',
86 |
87 | /**
88 | * The oi attribute is a log impression tag for impression logging
89 | * and action tracking. For an element that carries a jsaction
90 | * attribute, the element is identified for the purpose of
91 | * impression logging and click tracking by the dot separated path
92 | * of all oi attributes in the chain of ancestors of the element.
93 | *
94 | * Used by ActionFlow.
95 | */
96 | OI: 'oi',
97 |
98 | /**
99 | * The ved attribute is an encoded ClickTrackingCGI proto to track
100 | * visual elements.
101 | *
102 | * Used by ActionFlow.
103 | */
104 | VED: 'ved',
105 |
106 | /**
107 | * The vet attribute is the visual element type used to identify tracked
108 | * visual elements.
109 | */
110 | VET: 'vet',
111 |
112 | /**
113 | * Support for iteration on reprocessing.
114 | *
115 | * Used by ActionFlow.
116 | */
117 | JSINSTANCE: 'jsinstance',
118 |
119 | /**
120 | * All click jsactions that happen on the element that carries this
121 | * attribute or its descendants are automatically logged.
122 | * Impressions of jsactions on these elements are tracked too, if
123 | * requested by the impression() method of ActionFlow.
124 | *
125 | * Used by ActionFlow.
126 | */
127 | JSTRACK: 'jstrack'
128 | };
129 |
130 |
131 | /**
132 | * Special ActionFlow branch names defined by jsaction.
133 | * @enum {string}
134 | */
135 | jsaction.Branch = {
136 | /**
137 | * The main branch, i.e. the branch the action flow instance starts
138 | * right at construction.
139 | */
140 | MAIN: 'main-actionflow-branch'
141 | };
142 |
143 |
144 | /**
145 | * All special characters used by jsaction.
146 | * @const
147 | */
148 | jsaction.Char = {
149 | /**
150 | * The separator between the namespace and the action name in the
151 | * jsaction attribute value.
152 | */
153 | NAMESPACE_ACTION_SEPARATOR: '.',
154 |
155 | /**
156 | * The separator between the event name and action in the jsaction
157 | * attribute value.
158 | */
159 | EVENT_ACTION_SEPARATOR: ':',
160 |
161 | /**
162 | * The separator between the logged oi attribute values in the &oi=
163 | * URL parameter value.
164 | */
165 | OI_SEPARATOR: '.',
166 |
167 | /**
168 | * The separator between the key and the value pairs in the &cad=
169 | * URL parameter value.
170 | */
171 | CAD_KEY_VALUE_SEPARATOR: ':',
172 |
173 | /**
174 | * The separator between the key-value pairs in the &cad= URL
175 | * parameter value.
176 | */
177 | CAD_SEPARATOR: ','
178 | };
179 |
180 |
181 | /**
182 | * Names of events that are special to jsaction. These are not all
183 | * event types that are legal to use in either HTML or the addEvent()
184 | * API, but these are the ones that are treated specially. All other
185 | * DOM events can be used in either addEvent() or in the value of the
186 | * jsaction attribute. Beware of browser specific events or events
187 | * that don't bubble though: If they are not mentioned here, then
188 | * event contract doesn't work around their peculiarities.
189 | * @enum {string}
190 | */
191 | jsaction.EventType = {
192 | /**
193 | * Mouse middle click, introduced in Chrome 55 and not yet supported on
194 | * other browsers.
195 | */
196 | AUXCLICK: 'auxclick',
197 |
198 | /**
199 | * The change event fired by browsers when the `value` attribute of input,
200 | * select, and textarea elements are changed.
201 | */
202 | CHANGE: 'change',
203 |
204 | /**
205 | * The click event. In addEvent() refers to all click events, in the
206 | * jsaction attribute it refers to the unmodified click and Enter/Space
207 | * keypress events. In the latter case, a jsaction click will be triggered,
208 | * for accessibility reasons. See clickmod and clickonly, below.
209 | */
210 | CLICK: 'click',
211 |
212 | /**
213 | * Specifies the jsaction for a modified click event (i.e. a mouse
214 | * click with the modifier key Cmd/Ctrl pressed). This event isn't
215 | * separately enabled in addEvent(), because in the DOM, it's just a
216 | * click event.
217 | */
218 | CLICKMOD: 'clickmod',
219 |
220 | /**
221 | * Specifies the jsaction for a click-only event. Click-only doesn't take
222 | * into account the case where an element with focus receives an Enter/Space
223 | * keypress. This event isn't separately enabled in addEvent().
224 | */
225 | CLICKONLY: 'clickonly',
226 |
227 | /**
228 | * The dblclick event.
229 | */
230 | DBLCLICK: 'dblclick',
231 |
232 | /**
233 | * Focus doesn't bubble, but you can use it in addEvent() and
234 | * jsaction anyway. EventContract does the right thing under the
235 | * hood.
236 | */
237 | FOCUS: 'focus',
238 |
239 | /**
240 | * This event only exists in IE. For addEvent() and jsaction, use
241 | * focus instead; EventContract does the right thing even though
242 | * focus doesn't bubble.
243 | */
244 | FOCUSIN: 'focusin',
245 |
246 | /**
247 | * Analog to focus.
248 | */
249 | BLUR: 'blur',
250 |
251 | /**
252 | * Analog to focusin.
253 | */
254 | FOCUSOUT: 'focusout',
255 |
256 | /**
257 | * Submit doesn't bubble, so it cannot be used with event
258 | * contract. However, the browser helpfully fires a click event on
259 | * the submit button of a form (even if the form is not submitted by
260 | * a click on the submit button). So you should handle click on the
261 | * submit button instead.
262 | */
263 | SUBMIT: 'submit',
264 |
265 | /**
266 | * The keydown event. In addEvent() and non-click jsaction it represents the
267 | * regular DOM keydown event. It represents click actions in non-Gecko
268 | * browsers.
269 | */
270 | KEYDOWN: 'keydown',
271 |
272 | /**
273 | * The keypress event. In addEvent() and non-click jsaction it represents the
274 | * regular DOM keypress event. It represents click actions in Gecko browsers.
275 | */
276 | KEYPRESS: 'keypress',
277 |
278 | /**
279 | * The keyup event. In addEvent() and non-click jsaction it represents the
280 | * regular DOM keyup event. It represents click actions in non-Gecko
281 | * browsers.
282 | */
283 | KEYUP: 'keyup',
284 |
285 | /**
286 | * The mouseup event. Can either be used directly or used implicitly to
287 | * capture mouseup events. In addEvent(), it represents a regular DOM
288 | * mouseup event.
289 | */
290 | MOUSEUP: 'mouseup',
291 |
292 | /**
293 | * The mousedown event. Can either be used directly or used implicitly to
294 | * capture mouseenter events. In addEvent(), it represents a regular DOM
295 | * mouseover event.
296 | */
297 | MOUSEDOWN: 'mousedown',
298 |
299 | /**
300 | * The mouseover event. Can either be used directly or used implicitly to
301 | * capture mouseenter events. In addEvent(), it represents a regular DOM
302 | * mouseover event.
303 | */
304 | MOUSEOVER: 'mouseover',
305 |
306 | /**
307 | * The mouseout event. Can either be used directly or used implicitly to
308 | * capture mouseover events. In addEvent(), it represents a regular DOM
309 | * mouseout event.
310 | */
311 | MOUSEOUT: 'mouseout',
312 |
313 | /**
314 | * The mouseenter event. Does not bubble and fires individually on each
315 | * element being entered within a DOM tree.
316 | */
317 | MOUSEENTER: 'mouseenter',
318 |
319 | /**
320 | * The mouseleave event. Does not bubble and fires individually on each
321 | * element being entered within a DOM tree.
322 | */
323 | MOUSELEAVE: 'mouseleave',
324 |
325 | /**
326 | * The mousemove event.
327 | */
328 | MOUSEMOVE: 'mousemove',
329 |
330 | /**
331 | * The error event. The error event doesn't bubble, but you can use it in
332 | * addEvent() and jsaction anyway. EventContract does the right thing under
333 | * the hood (except in IE8 which does not use error events).
334 | */
335 | ERROR: 'error',
336 |
337 | /**
338 | * The load event. The load event doesn't bubble, but you can use it in
339 | * addEvent() and jsaction anyway. EventContract does the right thing
340 | * under the hood.
341 | */
342 | LOAD: 'load',
343 |
344 | /**
345 | * The unload event.
346 | */
347 | UNLOAD: 'unload',
348 |
349 | /**
350 | * The touchstart event. Bubbles, will only ever fire in browsers with
351 | * touch support.
352 | */
353 | TOUCHSTART: 'touchstart',
354 |
355 | /**
356 | * The touchend event. Bubbles, will only ever fire in browsers with
357 | * touch support.
358 | */
359 | TOUCHEND: 'touchend',
360 |
361 | /**
362 | * The touchmove event. Bubbles, will only ever fire in browsers with
363 | * touch support.
364 | */
365 | TOUCHMOVE: 'touchmove',
366 |
367 | /**
368 | * The input event.
369 | */
370 | INPUT: 'input',
371 |
372 | /**
373 | * The scroll event.
374 | */
375 | SCROLL: 'scroll',
376 |
377 | /**
378 | * A custom event. The actual custom event type is declared as the 'type'
379 | * field in the event details. Supported in Firefox 6+, IE 9+, and all Chrome
380 | * versions.
381 | *
382 | * This is an internal name. Users should use jsaction.fireCustomEvent to
383 | * fire custom events instead of relying on this type to create them.
384 | */
385 | CUSTOM: '_custom'
386 | };
387 |
388 |
389 | /**
390 | * Special keycodes used by jsaction for the generic click action.
391 | * @enum {number}
392 | */
393 | jsaction.KeyCodes = {
394 | /**
395 | * If on a Macintosh with an extended keyboard, the Enter key located in the
396 | * numeric pad has a different ASCII code.
397 | */
398 | MAC_ENTER: 3,
399 |
400 | /**
401 | * The Enter key.
402 | */
403 | ENTER: 13,
404 |
405 | /**
406 | * The Space key.
407 | */
408 | SPACE: 32
409 | };
410 |
411 |
412 | /**
413 | * Special tag names used by jsaction for the generic click action.
414 | * @enum {string}
415 | */
416 | jsaction.TagName = {
417 | /**
418 | * A textarea tag.
419 | */
420 | TEXTAREA: 'TEXTAREA',
421 |
422 | /**
423 | * An input tag.
424 | */
425 | INPUT: 'INPUT',
426 |
427 | /**
428 | * A button tag.
429 | */
430 | BUTTON: 'BUTTON',
431 |
432 | /**
433 | * An anchor tag.
434 | */
435 | A: 'A'
436 | };
437 |
438 |
439 | /**
440 | * Special names used by jsaction.
441 | * @enum {string}
442 | */
443 | jsaction.Name = {
444 | /**
445 | * The start time property of ActionFlow. TODO(user): Maybe a Property?
446 | */
447 | START: 'start',
448 |
449 | /**
450 | * Click additional data.
451 | */
452 | CAD: 'cad',
453 |
454 | /**
455 | * Action data to track duplicate ticks. This is used as a key in
456 | * additionalData map and in the value of the CLICK_ADDITIONAL_DATA
457 | * URL parameter in the reporting request.
458 | */
459 | DUP: 'dup'
460 | };
461 |
462 |
463 | /**
464 | * All the CSI ticks issued by jsaction.
465 | * @enum {string}
466 | */
467 | jsaction.Tick = {
468 | /**
469 | * Tick that indicates that the control flow enters ActionFlow.impression().
470 | */
471 | IMP0: 'imp0',
472 |
473 | /**
474 | * Tick that indicates that the control flow leaves impression().
475 | */
476 | IMP1: 'imp1'
477 | };
478 |
479 |
480 | /**
481 | * All properties that are used by jsaction.
482 | * @enum {string}
483 | */
484 | jsaction.Property = {
485 | /**
486 | * The parsed value of the jsaction attribute is stored in this
487 | * property on the DOM node. The parsed value is an Object. The
488 | * property names of the object are the events; the values are the
489 | * names of the actions. This property is attached even on nodes
490 | * that don't have a jsaction attribute as an optimization, because
491 | * property lookup is faster than attribute access.
492 | */
493 | JSACTION: '__jsaction',
494 |
495 | /**
496 | * The parsed value of the jsnamespace attribute is stored in this
497 | * property on the DOM node.
498 | */
499 | JSNAMESPACE: '__jsnamespace',
500 |
501 | /**
502 | * The value of the oi attribute as a property, for faster access.
503 | */
504 | OI: '__oi',
505 |
506 | /**
507 | * The owner property references an a logical owner for a DOM node. JSAction
508 | * will follow this reference instead of parentNode when traversing the DOM
509 | * to find jsaction attributes. This allows overlaying a logical structure
510 | * over a document where the DOM structure can't reflect that structure.
511 | */
512 | OWNER: '__owner'
513 | };
514 |
515 |
516 | jsaction.UrlParam = {
517 | /**
518 | * The type of the click is the name of the jsaction it was mapped to.
519 | */
520 | CLICK_TYPE: 'ct',
521 |
522 | /**
523 | * Click data contains the positional index of the clicked element
524 | * among its sibling as given by the jsinstance attribute value, if
525 | * any.
526 | */
527 | CLICK_DATA: 'cd',
528 |
529 | /**
530 | * Contains more structured data registered during the execution of
531 | * the jsaction handler and registered with the ActionFlow
532 | * instance. Among these data is informaiton about impressions that
533 | * were generated during the handling of the jsaction.
534 | */
535 | CLICK_ADDITIONAL_DATA: 'cad',
536 |
537 | /**
538 | * The event ID of the response that generated the clicked element,
539 | * as obtained from the value of the jstrack attribute.
540 | */
541 | EVENT_ID: 'ei',
542 |
543 | /**
544 | * The visual element data for the clicked element, as obtained from
545 | * the ved attribute.
546 | */
547 | VISUAL_ELEMENT_CLICK: 'ved',
548 |
549 | /**
550 | * The visual element type of the clicked element, as obtained from the vet
551 | * attribute.
552 | */
553 | VISUAL_ELEMENT_TYPE: 'vet'
554 | };
555 |
--------------------------------------------------------------------------------