├── 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 |
17 |
18 |
19 | foo1 20 |
21 |
22 |
23 |
24 |
25 |
26 | foo1 27 |
28 |
29 |
30 |
31 |
32 |
33 | baz 34 |
35 |
36 |
37 |
38 |
39 |
41 | baz 42 |
43 |
44 |
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 |
2 |
3 |
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 |
2 |
3 | 4 | 5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
40 | 41 | 42 |
43 |
44 |
45 | 46 |
47 |
48 |
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 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 | 87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 | 117 |
118 |
119 | 120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 |
128 |
129 |
130 |
131 |
132 |
133 | 134 |
135 |
136 |
137 |
138 | 139 |
140 |
141 |
142 |
143 | 144 |
145 | 146 | 147 | 148 |
149 | 150 |
151 | 152 |
153 | 154 |
155 | 158 |
159 | 160 |
161 | 164 |
165 | 166 |
167 | 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 |
2 |
3 | 4 | 5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
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 |
4 | 5 |
6 |
7 | 8 |
9 | 10 |
12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
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