├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile.js ├── README.md ├── config ├── copyright.txt ├── karma-conf.js └── testing-utils-karma-conf.js ├── dist ├── t3-jquery-2.7.0.js ├── t3-jquery-2.7.0.min.js ├── t3-jquery-testing-2.7.0.js ├── t3-jquery-testing.js ├── t3-jquery.js ├── t3-jquery.min.js ├── t3-jquery.min.js.map ├── t3-native-2.7.0.js ├── t3-native-2.7.0.min.js ├── t3-native-testing-2.7.0.js ├── t3-native-testing.js ├── t3-native.js ├── t3-native.min.js ├── t3-native.min.js.map ├── t3-testing.js ├── t3.js ├── t3.min.js └── t3.min.js.map ├── examples └── todo │ ├── bower_components │ ├── jquery-1.11.1.min │ │ ├── .bower.json │ │ └── index.js │ └── todomvc-common │ │ ├── base.css │ │ ├── base.js │ │ └── bg.png │ ├── css │ └── app.css │ ├── index.html │ └── js │ ├── app.js │ ├── behaviors │ └── todo.js │ ├── modules │ ├── footer.js │ ├── header.js │ ├── list.js │ └── page.js │ ├── services │ ├── router.js │ └── todos-db.js │ └── t3-dev-build.js ├── lib ├── .eslintrc ├── application-stub.js ├── application.js ├── box.js ├── context.js ├── dom-event-delegate.js ├── dom-jquery.js ├── dom-native.js ├── event-target.js ├── test-service-provider.js ├── wrap-end.partial └── wrap-start.partial ├── package.json └── tests ├── .eslintrc ├── api-test.js ├── application-test.js ├── context-test.js ├── dom-event-delegate-test.js ├── dom-test.js ├── event-target-test.js ├── test-service-provider-test.js └── utils ├── assert.js └── common-setup.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "window": false, 4 | "document": false, 5 | "Box": true, 6 | "$": false 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "no-alert": 2, 11 | "no-array-constructor": 2, 12 | "no-caller": 2, 13 | "no-catch-shadow": 2, 14 | "no-empty-label": 2, 15 | "no-eval": 2, 16 | "no-extend-native": 2, 17 | "no-extra-bind": 2, 18 | "no-extra-parens": [2, "functions"], 19 | "no-floating-decimal": 2, 20 | "no-implied-eval": 2, 21 | "no-iterator": 2, 22 | "no-label-var": 2, 23 | "no-labels": 2, 24 | "no-lone-blocks": 2, 25 | "no-loop-func": 2, 26 | "no-multi-spaces": 2, 27 | "no-multi-str": 2, 28 | "no-native-reassign": 2, 29 | "no-nested-ternary": 2, 30 | "no-new": 2, 31 | "no-new-func": 2, 32 | "no-new-object": 2, 33 | "no-new-wrappers": 2, 34 | "no-octal-escape": 2, 35 | "no-process-exit": 2, 36 | "no-proto": 2, 37 | "no-return-assign": 2, 38 | "no-script-url": 2, 39 | "no-sequences": 2, 40 | "no-shadow": 2, 41 | "no-shadow-restricted-names": 2, 42 | "no-spaced-func": 2, 43 | "no-trailing-spaces": 2, 44 | "no-undef-init": 2, 45 | "no-underscore-dangle": 0, 46 | "no-unused-expressions": 2, 47 | 48 | // We allow unused args 49 | "no-unused-vars": [2, {"args": "none"}], 50 | "no-use-before-define": 2, 51 | "no-with": 2, 52 | 53 | "brace-style": 2, 54 | "camelcase": 2, 55 | "comma-spacing": 2, 56 | "consistent-return": 2, 57 | "curly": [2, "all"], 58 | "dot-notation": [2, { "allowKeywords": true }], 59 | "eol-last": 2, 60 | "eqeqeq": 2, 61 | "func-style": [2, "declaration"], 62 | "guard-for-in": 2, 63 | "indent": [2, "tab"], 64 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 65 | "new-cap": 2, 66 | "new-parens": 2, 67 | "quotes": [2, "single"], 68 | "radix": 2, 69 | "semi": 2, 70 | "semi-spacing": [2, {"before": false, "after": true}], 71 | "space-after-keywords": 2, 72 | "space-infix-ops": 2, 73 | "space-return-throw-case": 2, 74 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 75 | "strict": [2, "function"], 76 | "wrap-iife": 2, 77 | "valid-jsdoc": [2, { 78 | "prefer": { 79 | "return": "returns" 80 | } 81 | }], 82 | "yoda": [2, "never"] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | node_modules 3 | doc 4 | *.swp 5 | *.iml 6 | .idea 7 | .DS_Store 8 | npm-debug.log 9 | dist/t3-[0-9]*.js 10 | dist/t3-testing-[0-9]*.js 11 | 12 | # Karama generated test coverage 13 | coverage-client 14 | /.dir-locals.el 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v2.7.0 - August 26, 2016 2 | 3 | * 2.7.0 (Jeff Tan) 4 | * Issue #159: Added setErrorHandler to override default error handling (Rey) 5 | 6 | v2.6.0 - July 13, 2016 7 | 8 | * 2.6.0 (Jeff Tan) 9 | * Add test for reportInfo() method (Mike) 10 | * Added new method to report console info messages in debug mode (#175) (Mike) 11 | * Update README.md (Jeff Tan) 12 | 13 | v2.5.0 - May 20, 2016 14 | 15 | * 2.5.0 (Jeff Tan) 16 | * Fixes constructor reference in new instances of Box.Context and Box.TestServiceProvider (fixes #172) (Adam Platti) 17 | 18 | v2.4.0 - March 23, 2016 19 | 20 | * 2.4.0 (Jeff Tan) 21 | * Add reportWarning to test service provider (Jeff Tan) 22 | * Add custom event types (Jeff Tan) 23 | * Add reportWarning to Box.Application (Jeff Tan) 24 | * Include the data-module element when finding event targets (Jeff Tan) 25 | * Update Copyright year (Jeff Tan) 26 | 27 | v2.3.0 - March 16, 2016 28 | 29 | * 2.3.0 (Jeff Tan) 30 | * Set the handler context to the actual behavior or module instance (Justin Bennett) 31 | 32 | v2.2.0 - March 14, 2016 33 | 34 | * 2.2.0 (Jeff Tan) 35 | * Use recommended wording (Justin Bennett) 36 | * Remove bind, reword test (Justin Bennett) 37 | * Remove unnecessary `bind` call. (Zephraph) 38 | * Support cleaner onmessage handler (Zephraph) 39 | * Add malformed JSON error message (Zephraph) 40 | 41 | v2.1.0 - January 31, 2016 42 | 43 | * 2.1.0 (Jeff Tan) 44 | * Add basic element check to DOMEventDelegate (Jeff Tan) 45 | * Allow getModuleConfig() to execute before module instantiation (Jeff Tan) 46 | 47 | v2.0.2 - November 18, 2015 48 | 49 | * 2.0.2 (Jeff Tan) 50 | * Remove package.json caching in the dist function (Jeff Tan) 51 | 52 | v2.0.1 - November 18, 2015 53 | 54 | * 2.0.1 (Jeff Tan) 55 | * Fix release script to work with multiple dist calls (Jeff Tan) 56 | * Add debug output to release script (Jeff Tan) 57 | 58 | v2.0.0 - November 18, 2015 59 | 60 | * 2.0.0 (Jeff Tan) 61 | * Add separate file header for testing package (Jeff Tan) 62 | * Fixing spaces in build script (Jeff Tan) 63 | * Return singleton service instance when getService is called on pre-registered services (Jeff Tan) 64 | * Add mousemove to allowed event types (Jeff Tan) 65 | * Update README.md (Jeff Tan) 66 | * Remove service exports from T3 (Jeff Tan) 67 | * Update Readme for 2.0.0 release and add auto-version updating (Jeff Tan) 68 | * Add hasService() to context object (Jeff Tan) 69 | * [Breaking] Add allowedServiceList to TestServiceProvider (Jeff Tan) 70 | * Use jQuery instead of $ for dom event delegation (Jeff Tan) 71 | * Add linting to test directory (Jeff Tan) 72 | * Breaking: Initialize behaviors before module (Jeff Tan) 73 | * Change getService() to throw error when requesting non-existent service. Add hasService() method. (Jeff Tan) 74 | * Bind event handlers after init() is called (Jeff Tan) 75 | * Throw error when duplicate behaviors are included (Jeff Tan) 76 | * Revert "Check for circular dependencies only during instantiation of service" (Denis Rodin) 77 | * Check for circular dependencies only during instantiation of service (Denis Rodin) 78 | * Check for circular dependencies only during instantiation of service (Denis Rodin) 79 | * Breaking: Use NativeDOM by default (fixes #76) (Nicholas C. Zakas) 80 | * Build: Upgrade ESLint (fixes #90) (Nicholas C. Zakas) 81 | 82 | v1.5.1 - August 10, 2015 83 | 84 | * 1.5.1 (Nicholas C. Zakas) 85 | * Fix: Ensure DOMEventDelegate is in T3 release (Nicholas C. Zakas) 86 | 87 | v1.5.0 - August 5, 2015 88 | 89 | * 1.5.0 (Nicholas C. Zakas) 90 | * Update: Make Box.Application chainable (fixes #65) (Nicholas C. Zakas) 91 | * New: Add Box.DOMEventDelegate (fixes #47, fixes #63) (Nicholas C. Zakas) 92 | * Update email address for support (Nicholas C. Zakas) 93 | 94 | v1.4.1 - June 24, 2015 95 | 96 | * 1.4.1 (Jeff Tan) 97 | * Ammended existing test to cover new fields (Jason Divock) 98 | * Making errors a bit more reportable (Jason Divock) 99 | 100 | v1.4.0 - June 11, 2015 101 | 102 | * 1.4.0 (Jeff Tan) 103 | * Add missing commonJS and AMD wrappers (Jeff Tan) 104 | 105 | v1.3.0 - June 9, 2015 106 | 107 | * 1.3.0 (Jeff Tan) 108 | * Breaking out dom events into an abstraction layer, building different versions of t3 accordingly (Jason Divock) 109 | * Add AMD support. Fixes #50 (Priyajeet Hora) 110 | * Fix release script (Nicholas C. Zakas) 111 | 112 | v1.2.0 - April 23, 2015 113 | 114 | * 1.2.0 (Nicholas C. Zakas) 115 | * Change karma to use mocha reporter (Jeff Tan) 116 | * Fix embedded sourcemap URL (fixes #31) (Nicholas C. Zakas) 117 | * Add wrapper to T3 for CommonJS (Jeff Tan) 118 | * Remove event delegation on detached nodes (Jeff Tan) 119 | * Update: Fire event when broadcast() is called (fixes #43) (Nicholas C. Zakas) 120 | * Reverting dist changes (Priyajeet Hora) 121 | * Prevent event-target.js duplicate handlers. Fixes #35. (Priyajeet Hora) 122 | * Clean up duplicated assign code (fixes #29) (azu) 123 | * Todo example fails to update completed items. Fixes #25 (Priyajeet Hora) 124 | * Firefox innerText fixes as well as switching the select all checkbox logic to use the model data instead of view elements Fixes #21 (Priyajeet Hora) 125 | * readme grammar (Matthew Hadley) 126 | * Apply or remove completed class when select all is clicked. Fixes #18. (Priyajeet Hora) 127 | * Adding missing doctype Fixes #14 (Priyajeet Hora) 128 | 129 | v1.1.1 - April 14, 2015 130 | 131 | * 1.1.1 (Nicholas C. Zakas) 132 | * Update build script to publish to npm and update site (Nicholas C. Zakas) 133 | * Add Travis configuration (Nicholas C. Zakas) 134 | * Add sourcemap generation (Nicholas C. Zakas) 135 | * Update README with badges and more info (Nicholas C. Zakas) 136 | * Missing semicolon in application-stub.js Fixes #4 (Priyajeet Hora) 137 | * Create t3-testing bundle (Jeff Tan) 138 | * Updating Karam coverage dir and adding it to gitignore (Tarrence van As) 139 | * Add ESLint to package.json (Jeff Tan) 140 | * Add license and contribution guide (Nicholas C. Zakas) 141 | * Rename t3 to t3js (Jeff Tan) 142 | * Adding items to .npmignore. Fixes #60. (Priyajeet Hora) 143 | * Adding items to .npmignore. Fixes #60. (Priyajeet Hora) 144 | * Adding npmignore. Fixes #60 (Priyajeet Hora) 145 | 146 | v1.1.0 - March 30, 2015 147 | 148 | * 1.1.0 (Nicholas C. Zakas) 149 | * Fix release task (Nicholas C. Zakas) 150 | * Add changelog generation (Nicholas C. Zakas) 151 | * Update README for open sourcing (Nicholas C. Zakas) 152 | * Remove most jQuery references (Nicholas C. Zakas) 153 | * Switch to ShellJS and create dist files (Nicholas C. Zakas) 154 | * Add focusin and focusout events (Jeff Tan) 155 | 156 | 1.0.2 - February 20, 2015 157 | 158 | * v1.0.2 (Jeff Tan) 159 | * Detect circular service dependencies (fixes #51) (Nicholas C. Zakas) 160 | -------------------------------------------------------------------------------- /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 | "License" shall mean the terms and conditions for use, reproduction, 9 | and distribution as defined by Sections 1 through 9 of this document. 10 | "Licensor" shall mean the copyright owner or entity authorized by 11 | the copyright owner that is granting the License. 12 | "Legal Entity" shall mean the union of the acting entity and all 13 | other entities that control, are controlled by, or are under common 14 | control with that entity. For the purposes of this definition, 15 | "control" means (i) the power, direct or indirect, to cause the 16 | direction or management of such entity, whether by contract or 17 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 18 | outstanding shares, or (iii) beneficial ownership of such entity. 19 | "You" (or "Your") shall mean an individual or Legal Entity 20 | exercising permissions granted by this License. 21 | "Source" form shall mean the preferred form for making modifications, 22 | including but not limited to software source code, documentation 23 | source, and configuration files. 24 | "Object" form shall mean any form resulting from mechanical 25 | transformation or translation of a Source form, including but 26 | not limited to compiled object code, generated documentation, 27 | and conversions to other media types. 28 | "Work" shall mean the work of authorship, whether in Source or 29 | Object form, made available under the License, as indicated by a 30 | copyright notice that is included in or attached to the work 31 | (an example is provided in the Appendix below). 32 | "Derivative Works" shall mean any work, whether in Source or Object 33 | form, that is based on (or derived from) the Work and for which the 34 | editorial revisions, annotations, elaborations, or other modifications 35 | represent, as a whole, an original work of authorship. For the purposes 36 | of this License, Derivative Works shall not include works that remain 37 | separable from, or merely link (or bind by name) to the interfaces of, 38 | the Work and Derivative Works thereof. 39 | "Contribution" shall mean any work of authorship, including 40 | the original version of the Work and any modifications or additions 41 | to that Work or Derivative Works thereof, that is intentionally 42 | submitted to Licensor for inclusion in the Work by the copyright owner 43 | or by an individual or Legal Entity authorized to submit on behalf of 44 | the copyright owner. For the purposes of this definition, "submitted" 45 | means any form of electronic, verbal, or written communication sent 46 | to the Licensor or its representatives, including but not limited to 47 | communication on electronic mailing lists, source code control systems, 48 | and issue tracking systems that are managed by, or on behalf of, the 49 | Licensor for the purpose of discussing and improving the Work, but 50 | excluding communication that is conspicuously marked or otherwise 51 | designated in writing by the copyright owner as "Not a Contribution." 52 | "Contributor" shall mean Licensor and any individual or Legal Entity 53 | on behalf of whom a Contribution has been received by Licensor and 54 | subsequently incorporated within the Work. 55 | 56 | 2. Grant of Copyright License. Subject to the terms and conditions of 57 | this License, each Contributor hereby grants to You a perpetual, 58 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 59 | copyright license to reproduce, prepare Derivative Works of, 60 | publicly display, publicly perform, sublicense, and distribute the 61 | Work and such Derivative Works in Source or Object form. 62 | 63 | 3. Grant of Patent License. Subject to the terms and conditions of 64 | this License, each Contributor hereby grants to You a perpetual, 65 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 66 | (except as stated in this section) patent license to make, have made, 67 | use, offer to sell, sell, import, and otherwise transfer the Work, 68 | where such license applies only to those patent claims licensable 69 | by such Contributor that are necessarily infringed by their 70 | Contribution(s) alone or by combination of their Contribution(s) 71 | with the Work to which such Contribution(s) was submitted. If You 72 | institute patent litigation against any entity (including a 73 | cross-claim or counterclaim in a lawsuit) alleging that the Work 74 | or a Contribution incorporated within the Work constitutes direct 75 | or contributory patent infringement, then any patent licenses 76 | granted to You under this License for that Work shall terminate 77 | as of the date such litigation is filed. 78 | 79 | 4. Redistribution. You may reproduce and distribute copies of the 80 | Work or Derivative Works thereof in any medium, with or without 81 | modifications, and in Source or Object form, provided that You 82 | meet the following conditions: 83 | 84 | (a) You must give any other recipients of the Work or 85 | Derivative Works a copy of this License; and 86 | 87 | (b) You must cause any modified files to carry prominent notices 88 | stating that You changed the files; and 89 | 90 | (c) You must retain, in the Source form of any Derivative Works 91 | that You distribute, all copyright, patent, trademark, and 92 | attribution notices from the Source form of the Work, 93 | excluding those notices that do not pertain to any part of 94 | the Derivative Works; and 95 | 96 | (d) If the Work includes a "NOTICE" text file as part of its 97 | distribution, then any Derivative Works that You distribute must 98 | include a readable copy of the attribution notices contained 99 | within such NOTICE file, excluding those notices that do not 100 | pertain to any part of the Derivative Works, in at least one 101 | of the following places: within a NOTICE text file distributed 102 | as part of the Derivative Works; within the Source form or 103 | documentation, if provided along with the Derivative Works; or, 104 | within a display generated by the Derivative Works, if and 105 | wherever such third-party notices normally appear. The contents 106 | of the NOTICE file are for informational purposes only and 107 | do not modify the License. You may add Your own attribution 108 | notices within Derivative Works that You distribute, alongside 109 | or as an addendum to the NOTICE text from the Work, provided 110 | that such additional attribution notices cannot be construed 111 | as modifying the License. 112 | 113 | You may add Your own copyright statement to Your modifications and 114 | may provide additional or different license terms and conditions 115 | for use, reproduction, or distribution of Your modifications, or 116 | for any such Derivative Works as a whole, provided Your use, 117 | reproduction, and distribution of the Work otherwise complies with 118 | the conditions stated in this License. 119 | 120 | 5. Submission of Contributions. Unless You explicitly state otherwise, 121 | any Contribution intentionally submitted for inclusion in the Work 122 | by You to the Licensor shall be under the terms and conditions of 123 | this License, without any additional terms or conditions. 124 | Notwithstanding the above, nothing herein shall supersede or modify 125 | the terms of any separate license agreement you may have executed 126 | with Licensor regarding such Contributions. 127 | 128 | 6. Trademarks. This License does not grant permission to use the trade 129 | names, trademarks, service marks, or product names of the Licensor, 130 | except as required for reasonable and customary use in describing the 131 | origin of the Work and reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. Unless required by applicable law or 134 | agreed to in writing, Licensor provides the Work (and each 135 | Contributor provides its Contributions) on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 137 | implied, including, without limitation, any warranties or conditions 138 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 139 | PARTICULAR PURPOSE. You are solely responsible for determining the 140 | appropriateness of using or redistributing the Work and assume any 141 | risks associated with Your exercise of permissions under this License. 142 | 143 | 8. Limitation of Liability. In no event and under no legal theory, 144 | whether in tort (including negligence), contract, or otherwise, 145 | unless required by applicable law (such as deliberate and grossly 146 | negligent acts) or agreed to in writing, shall any Contributor be 147 | liable to You for damages, including any direct, indirect, special, 148 | incidental, or consequential damages of any character arising as a 149 | result of this License or out of the use or inability to use the 150 | Work (including but not limited to damages for loss of goodwill, 151 | work stoppage, computer failure or malfunction, or any and all 152 | other commercial damages or losses), even if such Contributor 153 | has been advised of the possibility of such damages. 154 | 155 | 9. Accepting Warranty or Additional Liability. While redistributing 156 | the Work or Derivative Works thereof, You may choose to offer, 157 | and charge a fee for, acceptance of support, warranty, indemnity, 158 | or other liability obligations and/or rights consistent with this 159 | License. However, in accepting such obligations, You may act only 160 | on Your own behalf and on Your sole responsibility, not on behalf 161 | of any other Contributor, and only if You agree to indemnify, 162 | defend, and hold each Contributor harmless for any liability 163 | incurred by, or claims asserted against, such Contributor by reason 164 | of your accepting any such warranty or additional liability. 165 | 166 | END OF TERMS AND CONDITIONS 167 | -------------------------------------------------------------------------------- /Makefile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Build file 3 | * @author nzakas 4 | */ 5 | /*global config, target, exec, echo, find, which, test, exit, mkdir*/ 6 | 7 | 'use strict'; 8 | 9 | //------------------------------------------------------------------------------ 10 | // Requirements 11 | //------------------------------------------------------------------------------ 12 | 13 | require('shelljs/make'); 14 | 15 | var util = require('util'), 16 | path = require('path'), 17 | nodeCLI = require('shelljs-nodecli'), 18 | semver = require('semver'), 19 | dateformat = require('dateformat'), 20 | uglifyjs = require('uglify-js'); 21 | 22 | //------------------------------------------------------------------------------ 23 | // Data 24 | //------------------------------------------------------------------------------ 25 | 26 | var NODE = 'node ', // intentional extra space 27 | NODE_MODULES = './node_modules/', 28 | DIST_DIR = './dist/', 29 | 30 | // Utilities - intentional extra space at the end of each string 31 | JSDOC = NODE + NODE_MODULES + 'jsdoc/jsdoc.js ', 32 | 33 | // Since our npm package name is actually 't3js' 34 | DIST_NAME = 't3', 35 | DIST_JQUERY_NAME = DIST_NAME + '-jquery', 36 | DIST_NATIVE_NAME = DIST_NAME + '-native', 37 | 38 | // Directories 39 | JS_DIRS = getSourceDirectories(), 40 | 41 | // Files 42 | 43 | SRC_JQUERY_FILES = ['lib/wrap-start.partial', 'lib/box.js', 'lib/event-target.js', 'lib/dom-jquery.js', 'lib/dom-event-delegate.js', 'lib/context.js', 'lib/application.js', 'lib/wrap-end.partial'], 44 | SRC_NATIVE_FILES = ['lib/wrap-start.partial', 'lib/box.js', 'lib/event-target.js', 'lib/dom-native.js', 'lib/dom-event-delegate.js', 'lib/context.js', 'lib/application.js', 'lib/wrap-end.partial'], 45 | TESTING_JQUERY_FILES = ['lib/wrap-start.partial', 'lib/box.js', 'lib/event-target.js', 'lib/dom-jquery.js', 'lib/dom-event-delegate.js', 'lib/application-stub.js', 'lib/test-service-provider.js', 'lib/wrap-end.partial'], 46 | TESTING_NATIVE_FILES = ['lib/wrap-start.partial', 'lib/box.js', 'lib/event-target.js', 'lib/dom-native.js', 'lib/dom-event-delegate.js', 'lib/application-stub.js', 'lib/test-service-provider.js', 'lib/wrap-end.partial'], 47 | JS_FILES = find(JS_DIRS).filter(fileType('js')).join(' '), 48 | TEST_FILES = find('tests/').filter(fileType('js')).join(' '); 49 | 50 | //------------------------------------------------------------------------------ 51 | // Helpers 52 | //------------------------------------------------------------------------------ 53 | 54 | /** 55 | * Checks if current repository has any uncommitted changes 56 | * @return {boolean} 57 | */ 58 | function isDirectoryClean() { 59 | var fatalState = config.fatal; // save current fatal state 60 | config.fatal = false; 61 | var isUnstagedChanges = exec('git diff --exit-code', {silent:true}).code; 62 | var isStagedChanged = exec('git diff --cached --exit-code', {silent:true}).code; 63 | config.fatal = fatalState; // restore fatal state 64 | return !(isUnstagedChanges || isStagedChanged); 65 | } 66 | 67 | /** 68 | * Executes a Node CLI and exits with a non-zero exit code if the 69 | * CLI execution returns a non-zero exit code. Otherwise, it does 70 | * not exit. 71 | * @param {...string} [args] Arguments to pass to the Node CLI utility. 72 | * @returns {void} 73 | * @private 74 | */ 75 | function nodeExec(args) { 76 | args = arguments; // make linting happy 77 | var code = nodeCLI.exec.apply(nodeCLI, args).code; 78 | if (code !== 0) { 79 | exit(code); 80 | } 81 | } 82 | 83 | /** 84 | * Runs exec() but exits if the exit code is non-zero. 85 | * @param {string} cmd The command to execute. 86 | * @returns {void} 87 | * @private 88 | */ 89 | function execOrExit(cmd) { 90 | var code = exec(cmd).code; 91 | if (code !== 0) { 92 | exit(code); 93 | } 94 | } 95 | 96 | /** 97 | * Generates a function that matches files with a particular extension. 98 | * @param {string} extension The file extension (i.e. 'js') 99 | * @returns {Function} The function to pass into a filter method. 100 | * @private 101 | */ 102 | function fileType(extension) { 103 | return function(filename) { 104 | return filename.substring(filename.lastIndexOf('.') + 1) === extension; 105 | }; 106 | } 107 | 108 | /** 109 | * Determines which directories are present that might have JavaScript files. 110 | * @returns {string[]} An array of directories that exist. 111 | * @private 112 | */ 113 | function getSourceDirectories() { 114 | var dirs = [ 'lib', 'src', 'app' ], 115 | result = []; 116 | 117 | dirs.forEach(function(dir) { 118 | if (test('-d', dir)) { 119 | result.push(dir); 120 | } 121 | }); 122 | 123 | return result; 124 | } 125 | 126 | /** 127 | * Gets the git tags that represent versions. 128 | * @returns {string[]} An array of tags in the git repo. 129 | * @private 130 | */ 131 | function getVersionTags() { 132 | var tags = exec('git tag', { silent: true }).output.trim().split(/\n/g); 133 | 134 | return tags.reduce(function(list, tag) { 135 | if (semver.valid(tag)) { 136 | list.push(tag); 137 | } 138 | return list; 139 | }, []).sort(semver.compare); 140 | } 141 | 142 | /** 143 | * Verifies that common module loaders can be used with dist files 144 | * @returns {void} 145 | * @private 146 | */ 147 | function validateModuleLoading() { 148 | var t3js = require(DIST_DIR + DIST_NAME + '.js'); 149 | 150 | // Validate CommonJS 151 | if (!t3js || !('Application' in t3js)) { 152 | echo('ERROR: The dist file is not wrapped correctly for CommonJS'); 153 | exit(1); 154 | } 155 | } 156 | 157 | /** 158 | * Updates the README links with the latest version 159 | * @param string version The latest version string 160 | * @returns {void} 161 | * @private 162 | */ 163 | function updateReadme(version) { 164 | // Copy to temp file 165 | cat('README.md').to('README.tmp'); 166 | 167 | // Replace Version String 168 | sed('-i', /\/box\/t3js\/v([^/])+/g, '/box/t3js/' + version, 'README.tmp'); 169 | 170 | // Replace README 171 | rm('README.md'); 172 | mv('README.tmp', 'README.md'); 173 | } 174 | 175 | /** 176 | * Generate distribution files for a single package 177 | * @param Object config The distribution configuration 178 | * @returns {void} 179 | * @private 180 | */ 181 | function generateDistFiles(config) { 182 | // Delete package.json from the cache since it can get updated by npm version 183 | delete require.cache[require.resolve('./package.json')]; 184 | 185 | var pkg = require('./package.json'), 186 | distFilename = DIST_DIR + config.name + '.js', 187 | minDistFilename = distFilename.replace(/\.js$/, '.min.js'), 188 | minDistSourcemapFilename = minDistFilename + '.map', 189 | distTestingFilename = DIST_DIR + config.name + '-testing' + '.js'; 190 | 191 | // Add copyrights and version info 192 | var versionComment = '/*! ' + config.name + ' v' + pkg.version + ' */\n', 193 | testingVersionComment = '/*! ' + config.name + '-testing v' + pkg.version + ' */\n', 194 | copyrightComment = cat('./config/copyright.txt'); 195 | 196 | // concatenate files together and add version/copyright notices 197 | (versionComment + copyrightComment + cat(config.files)).to(distFilename); 198 | (testingVersionComment + copyrightComment + cat(config.testingFiles)).to(distTestingFilename); 199 | 200 | // create minified version with source maps 201 | var result = uglifyjs.minify(distFilename, { 202 | output: { 203 | comments: /^!/ 204 | }, 205 | outSourceMap: path.basename(minDistSourcemapFilename) 206 | }); 207 | result.code.to(minDistFilename); 208 | result.map.to(minDistSourcemapFilename); 209 | 210 | // create filenames with version in them 211 | cp(distFilename, distFilename.replace('.js', '-' + pkg.version + '.js')); 212 | cp(minDistFilename, minDistFilename.replace('.min.js', '-' + pkg.version + '.min.js')); 213 | cp(distTestingFilename, distTestingFilename.replace('.js', '-' + pkg.version + '.js')); 214 | } 215 | 216 | /** 217 | * Generate all distribution files 218 | * @private 219 | */ 220 | function dist() { 221 | if (test('-d', DIST_DIR)) { 222 | rm('-r', DIST_DIR + '*'); 223 | } else { 224 | mkdir(DIST_DIR); 225 | } 226 | 227 | [{ 228 | name: DIST_NATIVE_NAME, 229 | files: SRC_NATIVE_FILES, 230 | testingFiles: TESTING_NATIVE_FILES 231 | }, { 232 | name: DIST_JQUERY_NAME, 233 | files: SRC_JQUERY_FILES, 234 | testingFiles: TESTING_JQUERY_FILES 235 | }, { 236 | name: DIST_NAME, 237 | files: SRC_NATIVE_FILES, 238 | testingFiles: TESTING_NATIVE_FILES 239 | }].forEach(function(config){ 240 | generateDistFiles(config); 241 | }); 242 | } 243 | 244 | /** 245 | * Creates a release version tag and pushes to origin. 246 | * @param {string} type The type of release to do (patch, minor, major) 247 | * @returns {void} 248 | */ 249 | function release(type) { 250 | 251 | // 'npm version' needs a clean repository to run 252 | if (!isDirectoryClean()) { 253 | echo('RELEASE ERROR: Working directory must be clean to push release!'); 254 | exit(1); 255 | } 256 | 257 | echo('Running tests'); 258 | target.test(); 259 | 260 | // Step 1: Create the new version 261 | echo('Creating new version'); 262 | var newVersion = exec('npm version ' + type).output.trim(); 263 | 264 | // Step 2: Generate files 265 | echo('Generating dist files'); 266 | dist(); 267 | 268 | echo('Generating changelog'); 269 | target.changelog(); 270 | 271 | echo('Updating README'); 272 | updateReadme(newVersion); 273 | 274 | // Step 3: Validate CommonJS wrapping 275 | echo('Validating module loading'); 276 | validateModuleLoading(); 277 | 278 | // Step 4: Add files to current commit 279 | execOrExit('git add -A'); 280 | execOrExit('git commit --amend --no-edit'); 281 | 282 | // Step 5: reset the git tag to the latest commit 283 | execOrExit('git tag -f ' + newVersion); 284 | 285 | // Step 6: publish to git 286 | echo('Pushing to github'); 287 | execOrExit('git push origin master --tags'); 288 | 289 | // Step 7: publish to npm 290 | echo('Publishing to NPM'); 291 | execOrExit('npm publish'); 292 | 293 | // Step 8: Update version number in docs site 294 | echo('Updating documentation site'); 295 | execOrExit('git checkout gh-pages'); 296 | ('version: ' + newVersion).to('_data/t3.yml'); 297 | execOrExit('git commit -am "Update version number to ' + newVersion + '"'); 298 | execOrExit('git fetch origin && git rebase origin/gh-pages && git push origin gh-pages'); 299 | 300 | // Step 9: Switch back to master 301 | execOrExit('git checkout master'); 302 | 303 | // Step 10: Party time 304 | } 305 | 306 | 307 | //------------------------------------------------------------------------------ 308 | // Tasks 309 | //------------------------------------------------------------------------------ 310 | 311 | target.all = function() { 312 | target.test(); 313 | }; 314 | 315 | target.lint = function() { 316 | echo('Validating JavaScript files'); 317 | nodeExec('eslint', [JS_FILES, TEST_FILES].join(' ')); 318 | }; 319 | 320 | target.test = function() { 321 | target.lint(); 322 | 323 | echo('Running browser tests'); 324 | var code = exec('node ./node_modules/karma/bin/karma start config/karma-conf.js').code; 325 | if (code !== 0) { 326 | exit(code); 327 | } 328 | 329 | echo('Running Utilities tests'); 330 | target['utils-test'](); 331 | 332 | echo('Running API tests'); 333 | target['api-test'](); 334 | }; 335 | 336 | target['utils-test'] = function() { 337 | var code = exec('node ./node_modules/karma/bin/karma start config/testing-utils-karma-conf.js').code; 338 | if (code !== 0) { 339 | exit(code); 340 | } 341 | }; 342 | 343 | target['api-test'] = function() { 344 | // generate dist files that are used by api-test 345 | dist(); 346 | 347 | nodeExec('mocha', './tests/api-test.js'); 348 | 349 | // revert generated files 350 | execOrExit('git checkout dist'); 351 | }; 352 | 353 | target['test-watch'] = function() { 354 | echo('Watching files to run browser tests. Press Ctrl+C to exit.'); 355 | var code = exec('node ./node_modules/karma/bin/karma start config/karma-conf.js --single-run=false --autoWatch').code; 356 | if (code !== 0) { 357 | exit(code); 358 | } 359 | }; 360 | 361 | target.docs = function() { 362 | echo('Generating documentation'); 363 | exec(JSDOC + '-d jsdoc ' + JS_DIRS.join(' ')); 364 | echo('Documentation has been output to /jsdoc'); 365 | }; 366 | 367 | // Don't assign directly to dist since shelljs wraps this function 368 | target.dist = function() { 369 | dist(); 370 | }; 371 | 372 | target.changelog = function() { 373 | 374 | // get most recent two tags 375 | var tags = getVersionTags(), 376 | rangeTags = tags.slice(tags.length - 2), 377 | now = new Date(), 378 | timestamp = dateformat(now, 'mmmm d, yyyy'); 379 | 380 | // output header 381 | (rangeTags[1] + ' - ' + timestamp + '\n').to('CHANGELOG.tmp'); 382 | 383 | // get log statements 384 | var logs = exec('git log --pretty=format:"* %s (%an)" ' + rangeTags.join('..'), {silent: true}).output.split(/\n/g); 385 | logs = logs.filter(function(line) { 386 | return line.indexOf('Merge pull request') === -1 && line.indexOf('Merge branch') === -1; 387 | }); 388 | logs.push(''); // to create empty lines 389 | logs.unshift(''); 390 | 391 | // output log statements 392 | logs.join('\n').toEnd('CHANGELOG.tmp'); 393 | 394 | // switch-o change-o 395 | cat('CHANGELOG.tmp', 'CHANGELOG.md').to('CHANGELOG.md.tmp'); 396 | rm('CHANGELOG.tmp'); 397 | rm('CHANGELOG.md'); 398 | mv('CHANGELOG.md.tmp', 'CHANGELOG.md'); 399 | }; 400 | 401 | target.patch = function() { 402 | release('patch'); 403 | }; 404 | 405 | target.minor = function() { 406 | release('minor'); 407 | }; 408 | 409 | target.major = function() { 410 | release('major'); 411 | }; 412 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-image]][travis-url] 2 | [![Project Status](https://opensource.box.com/badges/deprecated.svg)](https://opensource.box.com/badges) 3 | 4 | # DEPRECATED 5 | 6 | Box has migrated using `react`, `webpack`, and the latest version of ECMAScript for our frontend projects as of 2018. We no longer support changes, pull requests, or upgrades to this package. We appreciate all of the user contributions that you have given us over the past few years. 7 | 8 | # T3 JavaScript Framework 9 | 10 | T3 is a client-side JavaScript framework for building large-scale web applications. Its design is based on the principles of [Scalable JavaScript Application Architecture](https://www.youtube.com/watch?v=mKouqShWI4o), specifically: 11 | 12 | * Enforcing loose coupling between components 13 | * Making dependencies explicit 14 | * Providing extension points to allow for unforeseen requirements 15 | * Abstracting away common pain points 16 | * Encouraging progressive enhancement 17 | 18 | The approaches taken in T3 have been battle-hardened through continuous production use at [Box](https://box.com) since 2013, where we use T3 along with jQuery, jQuery UI, and several other third-party libraries and frameworks. 19 | 20 | ## Framework Design 21 | 22 | T3 is different from most JavaScript frameworks. It's meant to be a small piece of an overall architecture that allows you to build scalable client-side code. 23 | 24 | ### No MVC Here 25 | 26 | T3 is explicitly *not* an MVC framework. It's a framework that allows the creation of loosely-coupled components while letting you decide what other pieces you need for your web application. You can use T3 with other frameworks like [Backbone](http://backbonejs.org/) or [React](http://facebook.github.io/react/), or you can use T3 by itself. If you decide you want model and views, in whatever form, you can still use them with T3. 27 | 28 | ### Unopinionated by Design 29 | 30 | T3 is made to be unopinionated while prescribing how some problems might be solved. Our goal here is not to create a single framework that can do everything for you, but rather, to provide some structure to your client-side code that allows you to make good choices. Then, you can add in other libraries and frameworks to suit your needs. 31 | 32 | ### Three Component Types 33 | 34 | T3 allows you to define functionality using just three component types: 35 | 36 | 1. **Services** are utility libraries that provide additional capabilities to your application. You can think of services as tools in a toolbox that you use to build an application. They intended to be reusable pieces of code such as cookie parsing, Ajax communication, string utilities, and so on. 37 | 2. **Modules** represent a particular DOM element on a page and manage the interaction inside of that element. It's a module's job to respond to user interaction within its boundaries. Your application is made up of a series of modules. Modules may not interact directly with other modules, but may do so indirectly. 38 | 3. **Behaviors** are mixins for modules and are used primarily to allow shared declarative event handling for modules without duplicating code. If, for instance, you use a particular attribute to indicate a link should use Ajax navigation instead of full-page navigation, you can share that functionality amongst multiple modules. 39 | 40 | We've found that by using a combination of these three component types, we're able to create compelling, progressively-enhanced user experiences. 41 | 42 | ## Installation 43 | 44 | To include T3 in a web page, you can use [RawGit](http://rawgit.com). 45 | 46 | The last published release: 47 | 48 | ``` 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ``` 61 | 62 | You may also use `bower` to install t3js: 63 | 64 | ``` 65 | bower install t3js 66 | ``` 67 | 68 | ## Upgrade from 1.5.1 to 2.0.0 69 | 70 | T3 2.0.0 was released on November 18th, 2015 with some major changes. To upgrade, please see [these instructions](http://t3js.org/docs/getting-started/migrating-to-2-0-0). 71 | 72 | ## Getting Started 73 | 74 | Your T3 front-end is made up of modules, so the first step is to indicate which modules are responsible for which parts of the page. You can do that by using the `data-module` attribute and specifying the module ID, such as: 75 | 76 | ```html 77 |
78 |

Box

79 | 80 |
81 | ``` 82 | 83 | This example specifies the module `header` should manage this particular part of the page. The module `header` is then defined as: 84 | 85 | ```js 86 | Box.Application.addModule('header', function(context) { 87 | 88 | return { 89 | 90 | onclick: function(event, element, elementType) { 91 | if (elementType === 'welcome-btn') { 92 | alert('Welcome, T3 user!'); 93 | } else { 94 | alert('You clicked outside the button.'); 95 | } 96 | } 97 | 98 | }; 99 | 100 | }); 101 | ``` 102 | 103 | This is a very simple module that has an `onclick` handler. T3 automatically wires up specified event handlers so you don't have to worry about using event delegation or removing event handlers when they are no longer needed. The `onclick` handler receives a DOM-normalized event object that can be used to get event details. When the button is clicked, a message is displayed. Additionally, clicking anywhere inside the module will display a different message. Event handlers are tied to the entire module area, so there's no need to attach multiple handlers of the same type. 104 | 105 | The last step is to initialize the T3 application: 106 | 107 | ```js 108 | Box.Application.init(); 109 | ``` 110 | 111 | This call starts all modules on the page (be sure to include both the T3 library and your module code before calling `init()`). We recommend calling `init()` as soon as possible after your JavaScript is loaded. Whether you do that `onload`, earlier, or later, is completely up to you. 112 | 113 | There are more extensive tutorials and examples on our [website](http://t3js.org). 114 | 115 | ## Browser Support 116 | 117 | T3 is tested and known to work in the following browsers: 118 | 119 | * Internet Explorer 9 and higher 120 | * Firefox (latest version) 121 | * Chrome (latest version) 122 | * Safari (latest version) 123 | 124 | With the exception of Internet Explorer, T3 will continue to support the current and previous one version of all major browsers. 125 | 126 | ## Contributing 127 | 128 | The main purpose of sharing T3 is to continue its development, making it faster, more efficient, and easier to use. 129 | 130 | ### Directory Structure 131 | 132 | * `config` - configuration files for the project 133 | * `dist` - browser bundles (this directory is updated automatically with each release) 134 | * `lib` - the source code as individual files 135 | * `tests` - the test code 136 | 137 | ### Prerequisites 138 | 139 | In order to get started contributing to T3, you'll need to be familiar and have installed: 140 | 141 | 1. [Git](http://git-scm.com/) 142 | 1. [npm](https://npmjs.org) 143 | 1. [Node.js](https://nodejs.org) or [IO.js](https://iojs.org) 144 | 145 | ### Setup 146 | 147 | Following the instructions in the [contributor guidelines](CONTRIBUTING.md) to setup a local copy of the T3 repository. 148 | 149 | Once you clone the T3 git repository, run the following inside the `t3js` directory: 150 | 151 | ``` 152 | $ npm i 153 | ``` 154 | 155 | This sets up all the dependencies that the T3 build system needs to function. 156 | 157 | **Note:** You'll need to do this periodically when pulling the latest code from our repository as dependencies might change. If you find unexpected errors, be sure to run `npm i` again to ensure your dependencies are up-to-date. 158 | 159 | ### Running Tests 160 | 161 | After that, you can run all tests by running: 162 | 163 | ``` 164 | $ npm test 165 | ``` 166 | 167 | This will start by linting the code and then running all unit tests. 168 | 169 | ### Build Commands 170 | 171 | The following build commands are available: 172 | 173 | 1. `npm test` - runs all linting and uni tests 174 | 1. `npm run lint` - runs all linting 175 | 1. `npm run dist` - creates the browser bundles and places them in `/dist` 176 | 177 | ## Frequently Asked Questions 178 | 179 | ### Can I use this with older browsers? 180 | 181 | We provide a custom version of T3 built with jQuery that is compatible with older browsers such as IE8. 182 | 183 | ### Why support IE8? 184 | 185 | The Box web application currently supports IE8 with a [planned end-of-life of December 31, 2015](https://support.box.com/hc/en-us/articles/200519838-What-Is-the-Box-Policy-for-Browser-and-OS-Support-). We will be dropping T3 support for IE8 at the same time. 186 | 187 | ## Support 188 | 189 | Need to contact us directly? Email [t3js@box.com](mailto:t3js@box.com) with your questions or comments. 190 | 191 | ## Copyright and License 192 | 193 | Copyright 2015 Box, Inc. All rights reserved. 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | 207 | [npm-image]: https://img.shields.io/npm/v/t3js.svg?style=flat-square 208 | [npm-url]: https://npmjs.org/package/t3js 209 | [travis-image]: https://img.shields.io/travis/box/t3js/master.svg?style=flat-square 210 | [travis-url]: https://travis-ci.org/box/t3js 211 | -------------------------------------------------------------------------------- /config/copyright.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright 2016 Box, Inc. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /config/karma-conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Karma configuration for T3 development. 3 | * @author nzakas 4 | */ 5 | 6 | "use strict"; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Requirements 10 | //------------------------------------------------------------------------------ 11 | 12 | // None! 13 | 14 | //------------------------------------------------------------------------------ 15 | // Public 16 | //------------------------------------------------------------------------------ 17 | 18 | module.exports = function(config) { 19 | 20 | config.set({ 21 | 22 | // base path that will be used to resolve all patterns (eg. files, exclude) 23 | basePath: '../', 24 | 25 | // frameworks to use 26 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 27 | frameworks: ['mocha'], 28 | 29 | // list of files / patterns to load in the browser 30 | files: [ 31 | 'tests/utils/assert.js', 32 | 'node_modules/assertive-chai/assertive-chai.js', 33 | 'node_modules/sinon/pkg/sinon.js', 34 | 'node_modules/jquery/dist/jquery.js', 35 | 'node_modules/leche/dist/leche.js', 36 | 'tests/utils/common-setup.js', 37 | 'lib/box.js', 38 | 'lib/dom-native.js', 39 | 'lib/dom-jquery.js', 40 | 'lib/dom-event-delegate.js', 41 | 'lib/context.js', 42 | 'lib/event-target.js', 43 | 'lib/application.js', 44 | 'tests/*.js' 45 | ], 46 | 47 | // list of files to exclude 48 | exclude: [ 49 | 'tests/api-test.js', 50 | 'tests/test-service-provider-test.js' 51 | ], 52 | 53 | // preprocess matching files before serving them to the browser 54 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 55 | preprocessors: { 56 | 'lib/*.js': ['coverage'] 57 | }, 58 | 59 | // web server port 60 | port: 9876, 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: false, 68 | 69 | // Continuous Integration mode 70 | // if true, Karma captures browsers, runs the tests and exits 71 | singleRun: true, 72 | 73 | // test results reporter to use 74 | // possible values: 'dots', 'progress' 75 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 76 | reporters: ['mocha', 'coverage', 'threshold'], 77 | 78 | // output HTML report of code coverage 79 | coverageReporter: { 80 | type: 'html', 81 | dir: 'coverage-client' 82 | }, 83 | 84 | // set coverage limits 85 | thresholdReporter: { 86 | statements: 94, 87 | branches: 80, 88 | functions: 98, 89 | lines: 94 90 | }, 91 | 92 | // output console.log calls to the console (default is false) 93 | captureConsole: true, 94 | 95 | // enable / disable colors in the output (reporters and logs) 96 | colors: true, 97 | 98 | // level of logging 99 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 100 | logLevel: config.LOG_INFO 101 | }); 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /config/testing-utils-karma-conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Karma configuration for T3 Testing Utilities 3 | * @author jtan 4 | */ 5 | 6 | "use strict"; 7 | 8 | //------------------------------------------------------------------------------ 9 | // Requirements 10 | //------------------------------------------------------------------------------ 11 | 12 | // None! 13 | 14 | //------------------------------------------------------------------------------ 15 | // Public 16 | //------------------------------------------------------------------------------ 17 | 18 | module.exports = function(config) { 19 | 20 | config.set({ 21 | 22 | // base path that will be used to resolve all patterns (eg. files, exclude) 23 | basePath: '../', 24 | 25 | // frameworks to use 26 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 27 | frameworks: ['mocha'], 28 | 29 | // list of files / patterns to load in the browser 30 | files: [ 31 | 'tests/utils/assert.js', 32 | 'node_modules/assertive-chai/assertive-chai.js', 33 | 'node_modules/sinon/pkg/sinon.js', 34 | 'node_modules/jquery/dist/jquery.js', 35 | 'node_modules/leche/dist/leche.js', 36 | 'tests/utils/common-setup.js', 37 | 'lib/box.js', 38 | 'lib/dom-native.js', 39 | 'lib/dom-jquery.js', 40 | 'lib/dom-event-delegate.js', 41 | 'lib/event-target.js', 42 | 'lib/application-stub.js', 43 | 'lib/test-service-provider.js', 44 | // Actual tests to run 45 | 'tests/test-service-provider-test.js', 46 | ], 47 | 48 | // list of files to exclude 49 | exclude: [ 50 | 'tests/api-test.js' 51 | ], 52 | 53 | // preprocess matching files before serving them to the browser 54 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 55 | preprocessors: { 56 | 'lib/test-service-provider.js': ['coverage'] 57 | }, 58 | 59 | // web server port 60 | port: 9876, 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: false, 68 | 69 | // Continuous Integration mode 70 | // if true, Karma captures browsers, runs the tests and exits 71 | singleRun: true, 72 | 73 | // test results reporter to use 74 | // possible values: 'dots', 'progress' 75 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 76 | reporters: ['mocha', 'coverage', 'threshold'], 77 | 78 | // output HTML report of code coverage 79 | coverageReporter: { 80 | type: 'html', 81 | dir: 'coverage-client' 82 | }, 83 | 84 | // set coverage limits 85 | thresholdReporter: { 86 | statements: 94, 87 | branches: 80, 88 | functions: 88, 89 | lines: 94 90 | }, 91 | 92 | // output console.log calls to the console (default is false) 93 | captureConsole: true, 94 | 95 | // enable / disable colors in the output (reporters and logs) 96 | colors: true, 97 | 98 | // level of logging 99 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 100 | logLevel: config.LOG_INFO 101 | }); 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /dist/t3-jquery-2.7.0.min.js: -------------------------------------------------------------------------------- 1 | /*! t3-jquery v2.7.0 */ 2 | /*! 3 | Copyright 2016 Box, Inc. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | !function(e){var t={};if(t.EventTarget=function(){"use strict";function e(){this._handlers={}}return e.prototype={constructor:e,on:function(e,t){var n,r,i=this._handlers[e];for("undefined"==typeof i&&(i=this._handlers[e]=[]),n=0,r=i.length;r>n;n++)if(i[n]===t)return;i.push(t)},fire:function(e,t){var n,r,i,o={type:e,data:t};if(n=this._handlers[o.type],n instanceof Array)for(n=n.concat(),r=0,i=n.length;i>r;r++)n[r].call(this,o)},off:function(e,t){var n,r,i=this._handlers[e];if(i instanceof Array)for(n=0,r=i.length;r>n;n++)if(i[n]===t){i.splice(n,1);break}}},e}(),t.JQueryDOM=function(){"use strict";return{type:"jquery",query:function(e,t){return jQuery(e).find(t)[0]||null},queryAll:function(e,t){return jQuery.makeArray(jQuery(e).find(t))},on:function(e,t,n){jQuery(e).on(t,n)},off:function(e,t,n){jQuery(e).off(t,n)}}}(),t.DOM=t.JQueryDOM,t.DOMEventDelegate=function(){"use strict";function e(e){return e&&e.hasAttribute("data-module")}function n(e){return e&&e.hasAttribute("data-type")}function r(t){for(var r=!1,i=!1;!r&&t&&t.parentNode&&!i;)r=n(t),i=e(t),r||(t=t.parentNode);return r?t:null}function i(e,t,n,r){var i,o;for(i=0;in;n++)if(e[n]===t)return n;return-1}function i(){m={},b={},E={},w=[],_={},D={},A=!1}function o(e){for(var t=0,n=w.length;n>t;t++)if(w[t]===e)return!0;return!1}function a(e){if("function"==typeof M)return void M(e);if(m.debug)throw e;O.fire("error",{exception:e})}function u(e,t){var n,r;for(n in e)r=e[n],"function"==typeof r&&(e[n]=function(e,n){return function(){var r=t+"."+e+"() - ";try{return n.apply(this,arguments)}catch(i){i.methodName=e,i.objectName=t,i.name=r+i.name,i.message=r+i.message,a(i)}}}(n,r))}function s(e){var t=e.getAttribute("data-module");return t?t.split(" ")[0]:""}function c(e,t){"function"==typeof e[t]&&e[t].apply(e,Array.prototype.slice.call(arguments,2))}function f(e){var t=E[e];if(t){if(!t.instance){if(o(e))return a(new ReferenceError("Circular service dependency: "+w.join(" -> ")+" -> "+e)),null;w.push(e),t.instance=t.creator(O),w.pop()}return t.instance}return a(new Error('Service "'+e+'" not found')),null}function l(e){var t,n,r,i,o,u=[],s={};for(n=e.instance.behaviors||[],t=0;tg;g++)h=p[g],c(h,"init");c(n.instance,"init"),d(n)}return this},stop:function(e){var t=g(e);if(t){p(t);for(var n,r=l(t),i=r.length-1;i>=0;i--)n=r[i],c(n,"destroy");c(t.instance,"destroy"),delete D[e.id]}else if(m.debug)return a(new Error("Unable to stop module associated with element: "+e.id)),this;return this},startAll:function(e){for(var n=t.DOM.queryAll(e,y),r=0,i=n.length;i>r;r++)this.start(n[r]);return this},stopAll:function(e){for(var n=t.DOM.queryAll(e,y),r=0,i=n.length;i>r;r++)this.stop(n[r]);return this},addModule:function(e,t){return"undefined"!=typeof b[e]?(a(new Error("Module "+e+" has already been added.")),this):(b[e]={creator:t,counter:1},this)},getModuleConfig:function(e,n){var r=g(e),i=null;if(r&&r.config)i=r.config;else{var o=t.DOM.query(e,'script[type="text/x-config"]');if(o)try{i=JSON.parse(o.text)}catch(u){a(new Error("Module with id "+e.id+" has a malformed config."))}r&&(r.config=i)}return i?"undefined"==typeof n?i:n in i?i[n]:null:null},addService:function(e,t){return"undefined"!=typeof E[e]?(a(new Error("Service "+e+" has already been added.")),this):(E[e]={creator:t,instance:null},this)},getService:f,hasService:function(e){return E.hasOwnProperty(e)},addBehavior:function(e,t){return"undefined"!=typeof _[e]?(a(new Error("Behavior "+e+" has already been added.")),this):(_[e]={creator:t,instance:null},this)},broadcast:function(e,t){var n,r,i,o,a;for(r in D)if(D.hasOwnProperty(r))for(i=D[r],v(i.instance,e,t),a=l(i),n=0;nn;n++)if(i[n]===t)return;i.push(t)},fire:function(e,t){var n,r,i,o={type:e,data:t};if(n=this._handlers[o.type],n instanceof Array)for(n=n.concat(),r=0,i=n.length;i>r;r++)n[r].call(this,o)},off:function(e,t){var n,r,i=this._handlers[e];if(i instanceof Array)for(n=0,r=i.length;r>n;n++)if(i[n]===t){i.splice(n,1);break}}},e}(),t.JQueryDOM=function(){"use strict";return{type:"jquery",query:function(e,t){return jQuery(e).find(t)[0]||null},queryAll:function(e,t){return jQuery.makeArray(jQuery(e).find(t))},on:function(e,t,n){jQuery(e).on(t,n)},off:function(e,t,n){jQuery(e).off(t,n)}}}(),t.DOM=t.JQueryDOM,t.DOMEventDelegate=function(){"use strict";function e(e){return e&&e.hasAttribute("data-module")}function n(e){return e&&e.hasAttribute("data-type")}function r(t){for(var r=!1,i=!1;!r&&t&&t.parentNode&&!i;)r=n(t),i=e(t),r||(t=t.parentNode);return r?t:null}function i(e,t,n,r){var i,o;for(i=0;in;n++)if(e[n]===t)return n;return-1}function i(){m={},b={},E={},w=[],_={},D={},A=!1}function o(e){for(var t=0,n=w.length;n>t;t++)if(w[t]===e)return!0;return!1}function a(e){if("function"==typeof M)return void M(e);if(m.debug)throw e;O.fire("error",{exception:e})}function u(e,t){var n,r;for(n in e)r=e[n],"function"==typeof r&&(e[n]=function(e,n){return function(){var r=t+"."+e+"() - ";try{return n.apply(this,arguments)}catch(i){i.methodName=e,i.objectName=t,i.name=r+i.name,i.message=r+i.message,a(i)}}}(n,r))}function s(e){var t=e.getAttribute("data-module");return t?t.split(" ")[0]:""}function c(e,t){"function"==typeof e[t]&&e[t].apply(e,Array.prototype.slice.call(arguments,2))}function f(e){var t=E[e];if(t){if(!t.instance){if(o(e))return a(new ReferenceError("Circular service dependency: "+w.join(" -> ")+" -> "+e)),null;w.push(e),t.instance=t.creator(O),w.pop()}return t.instance}return a(new Error('Service "'+e+'" not found')),null}function l(e){var t,n,r,i,o,u=[],s={};for(n=e.instance.behaviors||[],t=0;tg;g++)h=p[g],c(h,"init");c(n.instance,"init"),d(n)}return this},stop:function(e){var t=g(e);if(t){p(t);for(var n,r=l(t),i=r.length-1;i>=0;i--)n=r[i],c(n,"destroy");c(t.instance,"destroy"),delete D[e.id]}else if(m.debug)return a(new Error("Unable to stop module associated with element: "+e.id)),this;return this},startAll:function(e){for(var n=t.DOM.queryAll(e,y),r=0,i=n.length;i>r;r++)this.start(n[r]);return this},stopAll:function(e){for(var n=t.DOM.queryAll(e,y),r=0,i=n.length;i>r;r++)this.stop(n[r]);return this},addModule:function(e,t){return"undefined"!=typeof b[e]?(a(new Error("Module "+e+" has already been added.")),this):(b[e]={creator:t,counter:1},this)},getModuleConfig:function(e,n){var r=g(e),i=null;if(r&&r.config)i=r.config;else{var o=t.DOM.query(e,'script[type="text/x-config"]');if(o)try{i=JSON.parse(o.text)}catch(u){a(new Error("Module with id "+e.id+" has a malformed config."))}r&&(r.config=i)}return i?"undefined"==typeof n?i:n in i?i[n]:null:null},addService:function(e,t){return"undefined"!=typeof E[e]?(a(new Error("Service "+e+" has already been added.")),this):(E[e]={creator:t,instance:null},this)},getService:f,hasService:function(e){return E.hasOwnProperty(e)},addBehavior:function(e,t){return"undefined"!=typeof _[e]?(a(new Error("Behavior "+e+" has already been added.")),this):(_[e]={creator:t,instance:null},this)},broadcast:function(e,t){var n,r,i,o,a;for(r in D)if(D.hasOwnProperty(r))for(i=D[r],v(i.instance,e,t),a=l(i),n=0;nn;n++)if(i[n]===t)return;i.push(t)},fire:function(e,t){var n,r,i,o={type:e,data:t};if(n=this._handlers[o.type],n instanceof Array)for(n=n.concat(),r=0,i=n.length;i>r;r++)n[r].call(this,o)},off:function(e,t){var n,r,i=this._handlers[e];if(i instanceof Array)for(n=0,r=i.length;r>n;n++)if(i[n]===t){i.splice(n,1);break}}},e}(),t.NativeDOM=function(){"use strict";return{type:"native",query:function(e,t){return e.querySelector(t)},queryAll:function(e,t){return e.querySelectorAll(t)},on:function(e,t,n){e.addEventListener(t,n,!1)},off:function(e,t,n){e.removeEventListener(t,n,!1)}}}(),t.DOM=t.NativeDOM,t.DOMEventDelegate=function(){"use strict";function e(e){return e&&e.hasAttribute("data-module")}function n(e){return e&&e.hasAttribute("data-type")}function r(t){for(var r=!1,i=!1;!r&&t&&t.parentNode&&!i;)r=n(t),i=e(t),r||(t=t.parentNode);return r?t:null}function i(e,t,n,r){var i,o;for(i=0;in;n++)if(e[n]===t)return n;return-1}function i(){y={},b={},E={},w=[],_={},D={},A=!1}function o(e){for(var t=0,n=w.length;n>t;t++)if(w[t]===e)return!0;return!1}function a(e){if("function"==typeof M)return void M(e);if(y.debug)throw e;O.fire("error",{exception:e})}function u(e,t){var n,r;for(n in e)r=e[n],"function"==typeof r&&(e[n]=function(e,n){return function(){var r=t+"."+e+"() - ";try{return n.apply(this,arguments)}catch(i){i.methodName=e,i.objectName=t,i.name=r+i.name,i.message=r+i.message,a(i)}}}(n,r))}function s(e){var t=e.getAttribute("data-module");return t?t.split(" ")[0]:""}function c(e,t){"function"==typeof e[t]&&e[t].apply(e,Array.prototype.slice.call(arguments,2))}function f(e){var t=E[e];if(t){if(!t.instance){if(o(e))return a(new ReferenceError("Circular service dependency: "+w.join(" -> ")+" -> "+e)),null;w.push(e),t.instance=t.creator(O),w.pop()}return t.instance}return a(new Error('Service "'+e+'" not found')),null}function l(e){var t,n,r,i,o,u=[],s={};for(n=e.instance.behaviors||[],t=0;tv;v++)h=p[v],c(h,"init");c(n.instance,"init"),d(n)}return this},stop:function(e){var t=v(e);if(t){p(t);for(var n,r=l(t),i=r.length-1;i>=0;i--)n=r[i],c(n,"destroy");c(t.instance,"destroy"),delete D[e.id]}else if(y.debug)return a(new Error("Unable to stop module associated with element: "+e.id)),this;return this},startAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.start(n[r]);return this},stopAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.stop(n[r]);return this},addModule:function(e,t){return"undefined"!=typeof b[e]?(a(new Error("Module "+e+" has already been added.")),this):(b[e]={creator:t,counter:1},this)},getModuleConfig:function(e,n){var r=v(e),i=null;if(r&&r.config)i=r.config;else{var o=t.DOM.query(e,'script[type="text/x-config"]');if(o)try{i=JSON.parse(o.text)}catch(u){a(new Error("Module with id "+e.id+" has a malformed config."))}r&&(r.config=i)}return i?"undefined"==typeof n?i:n in i?i[n]:null:null},addService:function(e,t){return"undefined"!=typeof E[e]?(a(new Error("Service "+e+" has already been added.")),this):(E[e]={creator:t,instance:null},this)},getService:f,hasService:function(e){return E.hasOwnProperty(e)},addBehavior:function(e,t){return"undefined"!=typeof _[e]?(a(new Error("Behavior "+e+" has already been added.")),this):(_[e]={creator:t,instance:null},this)},broadcast:function(e,t){var n,r,i,o,a;for(r in D)if(D.hasOwnProperty(r))for(i=D[r],g(i.instance,e,t),a=l(i),n=0;nn;n++)if(i[n]===t)return;i.push(t)},fire:function(e,t){var n,r,i,o={type:e,data:t};if(n=this._handlers[o.type],n instanceof Array)for(n=n.concat(),r=0,i=n.length;i>r;r++)n[r].call(this,o)},off:function(e,t){var n,r,i=this._handlers[e];if(i instanceof Array)for(n=0,r=i.length;r>n;n++)if(i[n]===t){i.splice(n,1);break}}},e}(),t.NativeDOM=function(){"use strict";return{type:"native",query:function(e,t){return e.querySelector(t)},queryAll:function(e,t){return e.querySelectorAll(t)},on:function(e,t,n){e.addEventListener(t,n,!1)},off:function(e,t,n){e.removeEventListener(t,n,!1)}}}(),t.DOM=t.NativeDOM,t.DOMEventDelegate=function(){"use strict";function e(e){return e&&e.hasAttribute("data-module")}function n(e){return e&&e.hasAttribute("data-type")}function r(t){for(var r=!1,i=!1;!r&&t&&t.parentNode&&!i;)r=n(t),i=e(t),r||(t=t.parentNode);return r?t:null}function i(e,t,n,r){var i,o;for(i=0;in;n++)if(e[n]===t)return n;return-1}function i(){y={},b={},E={},w=[],_={},D={},A=!1}function o(e){for(var t=0,n=w.length;n>t;t++)if(w[t]===e)return!0;return!1}function a(e){if("function"==typeof M)return void M(e);if(y.debug)throw e;O.fire("error",{exception:e})}function u(e,t){var n,r;for(n in e)r=e[n],"function"==typeof r&&(e[n]=function(e,n){return function(){var r=t+"."+e+"() - ";try{return n.apply(this,arguments)}catch(i){i.methodName=e,i.objectName=t,i.name=r+i.name,i.message=r+i.message,a(i)}}}(n,r))}function s(e){var t=e.getAttribute("data-module");return t?t.split(" ")[0]:""}function c(e,t){"function"==typeof e[t]&&e[t].apply(e,Array.prototype.slice.call(arguments,2))}function f(e){var t=E[e];if(t){if(!t.instance){if(o(e))return a(new ReferenceError("Circular service dependency: "+w.join(" -> ")+" -> "+e)),null;w.push(e),t.instance=t.creator(O),w.pop()}return t.instance}return a(new Error('Service "'+e+'" not found')),null}function l(e){var t,n,r,i,o,u=[],s={};for(n=e.instance.behaviors||[],t=0;tv;v++)h=p[v],c(h,"init");c(n.instance,"init"),d(n)}return this},stop:function(e){var t=v(e);if(t){p(t);for(var n,r=l(t),i=r.length-1;i>=0;i--)n=r[i],c(n,"destroy");c(t.instance,"destroy"),delete D[e.id]}else if(y.debug)return a(new Error("Unable to stop module associated with element: "+e.id)),this;return this},startAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.start(n[r]);return this},stopAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.stop(n[r]);return this},addModule:function(e,t){return"undefined"!=typeof b[e]?(a(new Error("Module "+e+" has already been added.")),this):(b[e]={creator:t,counter:1},this)},getModuleConfig:function(e,n){var r=v(e),i=null;if(r&&r.config)i=r.config;else{var o=t.DOM.query(e,'script[type="text/x-config"]');if(o)try{i=JSON.parse(o.text)}catch(u){a(new Error("Module with id "+e.id+" has a malformed config."))}r&&(r.config=i)}return i?"undefined"==typeof n?i:n in i?i[n]:null:null},addService:function(e,t){return"undefined"!=typeof E[e]?(a(new Error("Service "+e+" has already been added.")),this):(E[e]={creator:t,instance:null},this)},getService:f,hasService:function(e){return E.hasOwnProperty(e)},addBehavior:function(e,t){return"undefined"!=typeof _[e]?(a(new Error("Behavior "+e+" has already been added.")),this):(_[e]={creator:t,instance:null},this)},broadcast:function(e,t){var n,r,i,o,a;for(r in D)if(D.hasOwnProperty(r))for(i=D[r],g(i.instance,e,t),a=l(i),n=0;nn;n++)if(i[n]===t)return;i.push(t)},fire:function(e,t){var n,r,i,o={type:e,data:t};if(n=this._handlers[o.type],n instanceof Array)for(n=n.concat(),r=0,i=n.length;i>r;r++)n[r].call(this,o)},off:function(e,t){var n,r,i=this._handlers[e];if(i instanceof Array)for(n=0,r=i.length;r>n;n++)if(i[n]===t){i.splice(n,1);break}}},e}(),t.NativeDOM=function(){"use strict";return{type:"native",query:function(e,t){return e.querySelector(t)},queryAll:function(e,t){return e.querySelectorAll(t)},on:function(e,t,n){e.addEventListener(t,n,!1)},off:function(e,t,n){e.removeEventListener(t,n,!1)}}}(),t.DOM=t.NativeDOM,t.DOMEventDelegate=function(){"use strict";function e(e){return e&&e.hasAttribute("data-module")}function n(e){return e&&e.hasAttribute("data-type")}function r(t){for(var r=!1,i=!1;!r&&t&&t.parentNode&&!i;)r=n(t),i=e(t),r||(t=t.parentNode);return r?t:null}function i(e,t,n,r){var i,o;for(i=0;in;n++)if(e[n]===t)return n;return-1}function i(){y={},b={},E={},w=[],_={},D={},A=!1}function o(e){for(var t=0,n=w.length;n>t;t++)if(w[t]===e)return!0;return!1}function a(e){if("function"==typeof M)return void M(e);if(y.debug)throw e;O.fire("error",{exception:e})}function u(e,t){var n,r;for(n in e)r=e[n],"function"==typeof r&&(e[n]=function(e,n){return function(){var r=t+"."+e+"() - ";try{return n.apply(this,arguments)}catch(i){i.methodName=e,i.objectName=t,i.name=r+i.name,i.message=r+i.message,a(i)}}}(n,r))}function s(e){var t=e.getAttribute("data-module");return t?t.split(" ")[0]:""}function c(e,t){"function"==typeof e[t]&&e[t].apply(e,Array.prototype.slice.call(arguments,2))}function f(e){var t=E[e];if(t){if(!t.instance){if(o(e))return a(new ReferenceError("Circular service dependency: "+w.join(" -> ")+" -> "+e)),null;w.push(e),t.instance=t.creator(O),w.pop()}return t.instance}return a(new Error('Service "'+e+'" not found')),null}function l(e){var t,n,r,i,o,u=[],s={};for(n=e.instance.behaviors||[],t=0;tv;v++)h=p[v],c(h,"init");c(n.instance,"init"),d(n)}return this},stop:function(e){var t=v(e);if(t){p(t);for(var n,r=l(t),i=r.length-1;i>=0;i--)n=r[i],c(n,"destroy");c(t.instance,"destroy"),delete D[e.id]}else if(y.debug)return a(new Error("Unable to stop module associated with element: "+e.id)),this;return this},startAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.start(n[r]);return this},stopAll:function(e){for(var n=t.DOM.queryAll(e,m),r=0,i=n.length;i>r;r++)this.stop(n[r]);return this},addModule:function(e,t){return"undefined"!=typeof b[e]?(a(new Error("Module "+e+" has already been added.")),this):(b[e]={creator:t,counter:1},this)},getModuleConfig:function(e,n){var r=v(e),i=null;if(r&&r.config)i=r.config;else{var o=t.DOM.query(e,'script[type="text/x-config"]');if(o)try{i=JSON.parse(o.text)}catch(u){a(new Error("Module with id "+e.id+" has a malformed config."))}r&&(r.config=i)}return i?"undefined"==typeof n?i:n in i?i[n]:null:null},addService:function(e,t){return"undefined"!=typeof E[e]?(a(new Error("Service "+e+" has already been added.")),this):(E[e]={creator:t,instance:null},this)},getService:f,hasService:function(e){return E.hasOwnProperty(e)},addBehavior:function(e,t){return"undefined"!=typeof _[e]?(a(new Error("Behavior "+e+" has already been added.")),this):(_[e]={creator:t,instance:null},this)},broadcast:function(e,t){var n,r,i,o,a;for(r in D)if(D.hasOwnProperty(r))for(i=D[r],g(i.instance,e,t),a=l(i),n=0;n 0) { 11 | return location.pathname.replace(/todomvc\//, ''); 12 | } 13 | return location.pathname; 14 | } 15 | 16 | function appendSourceLink() { 17 | var sourceLink = document.createElement('a'); 18 | var paragraph = document.createElement('p'); 19 | var footer = document.getElementById('info'); 20 | var urlBase = 'https://github.com/tastejs/todomvc/tree/gh-pages'; 21 | 22 | if (footer) { 23 | sourceLink.href = urlBase + getSourcePath(); 24 | sourceLink.appendChild(document.createTextNode('Check out the source')); 25 | paragraph.appendChild(sourceLink); 26 | footer.appendChild(paragraph); 27 | } 28 | } 29 | 30 | function redirect() { 31 | if (location.hostname === 'tastejs.github.io') { 32 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 33 | } 34 | } 35 | 36 | appendSourceLink(); 37 | redirect(); 38 | })(); 39 | -------------------------------------------------------------------------------- /examples/todo/bower_components/todomvc-common/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/box/t3js/334be4358567eeb9092d89667ea01810857d75a9/examples/todo/bower_components/todomvc-common/bg.png -------------------------------------------------------------------------------- /examples/todo/css/app.css: -------------------------------------------------------------------------------- 1 | #clear-completed { 2 | display: none; 3 | } 4 | .has-completed-tasks #clear-completed { 5 | display: block; 6 | } -------------------------------------------------------------------------------- /examples/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | T3 • TodoNotMVC 7 | 8 | 9 | 10 | 11 |
12 | 18 |
19 | 20 | 21 |
    22 |
23 | 33 |
34 |
35 | 0 items left 36 | 47 | 48 |
49 |
50 |
51 |

Double-click to edit a todo

52 |

Created by Box

53 |

Part of TodoMVC

54 |
55 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/todo/js/app.js: -------------------------------------------------------------------------------- 1 | // Since T3 exports Box as a namespace, assign a shortcut Application to Box.Application 2 | var Application = Box.Application; 3 | -------------------------------------------------------------------------------- /examples/todo/js/behaviors/todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview The TODO item behavior 3 | * @author Box 4 | */ 5 | 6 | /*global Box*/ 7 | 8 | /* 9 | * Handles behavior of todo items 10 | */ 11 | Application.addBehavior('todo', function(context) { 12 | 13 | 'use strict'; 14 | 15 | //-------------------------------------------------------------------------- 16 | // Private 17 | //-------------------------------------------------------------------------- 18 | 19 | var ENTER_KEY = 13, 20 | ESCAPE_KEY = 27; 21 | 22 | var todosDB, 23 | moduleEl; 24 | 25 | /** 26 | * Returns the nearest todo element 27 | * @param {HTMLElement} element A root/child node of a todo element to search from 28 | * @returns {HTMLElement} 29 | * @private 30 | */ 31 | function getClosestTodoElement(element) { 32 | 33 | var matchesSelector = element.matches || element.webkitMatchesSelector || element.mozMatchesSelector || element.msMatchesSelector; 34 | 35 | while (element) { 36 | if (matchesSelector.bind(element)('li')) { 37 | return element; 38 | } else { 39 | element = element.parentNode; 40 | } 41 | } 42 | return false; 43 | 44 | } 45 | 46 | /** 47 | * Returns the id of the nearest todo element 48 | * @param {HTMLElement} element A root/child node of a todo element 49 | * @returns {string} 50 | * @private 51 | */ 52 | function getClosestTodoId(element) { 53 | var todoEl = getClosestTodoElement(element); 54 | return todoEl.getAttribute('data-todo-id'); 55 | } 56 | 57 | //-------------------------------------------------------------------------- 58 | // Public 59 | //-------------------------------------------------------------------------- 60 | 61 | return { 62 | 63 | /** 64 | * Initializes the module. Caches a data store object to todos 65 | * @returns {void} 66 | */ 67 | init: function() { 68 | todosDB = context.getService('todos-db'); 69 | moduleEl = context.getElement(); 70 | }, 71 | 72 | /** 73 | * Destroys the module. 74 | * @returns {void} 75 | */ 76 | destroy: function() { 77 | todosDB = null; 78 | moduleEl = null; 79 | }, 80 | 81 | /** 82 | * Handles all click events for the behavior. 83 | * @param {Event} event A DOM-normalized event object. 84 | * @param {HTMLElement} element The nearest HTML element with a data-type 85 | * attribute specified or null if there is none. 86 | * @param {string} elementType The value of data-type for the nearest 87 | * element with that attribute specified or null if there is none. 88 | * @returns {void} 89 | */ 90 | onclick: function(event, element, elementType) { 91 | 92 | if (elementType === 'delete-btn') { 93 | var todoEl = getClosestTodoElement(element), 94 | todoId = getClosestTodoId(element); 95 | 96 | moduleEl.querySelector('#todo-list').removeChild(todoEl); 97 | todosDB.remove(todoId); 98 | 99 | context.broadcast('todoremoved', { 100 | id: todoId 101 | }); 102 | 103 | } 104 | 105 | }, 106 | 107 | /** 108 | * Handles change events for the behavior. 109 | * @param {Event} event A DOM-normalized event object. 110 | * @param {HTMLElement} element The nearest HTML element with a data-type 111 | * attribute specified or null if there is none. 112 | * @param {string} elementType The value of data-type for the nearest 113 | * element with that attribute specified or null if there is none. 114 | * @returns {void} 115 | */ 116 | onchange: function(event, element, elementType) { 117 | 118 | if (elementType === 'mark-as-complete-checkbox') { 119 | var todoEl = getClosestTodoElement(element), 120 | todoId = getClosestTodoId(element); 121 | 122 | if (element.checked) { 123 | todoEl.classList.add('completed'); 124 | todosDB.markAsComplete(todoId); 125 | } else { 126 | todoEl.classList.remove('completed'); 127 | todosDB.markAsIncomplete(todoId); 128 | } 129 | 130 | context.broadcast('todostatuschange'); 131 | 132 | } 133 | 134 | }, 135 | 136 | /** 137 | * Handles double click events for the behavior. 138 | * @param {Event} event A DOM-normalized event object. 139 | * @param {HTMLElement} element The nearest HTML element with a data-type 140 | * attribute specified or null if there is none. 141 | * @param {string} elementType The value of data-type for the nearest 142 | * element with that attribute specified or null if there is none. 143 | * @returns {void} 144 | */ 145 | ondblclick: function(event, element, elementType) { 146 | 147 | if (elementType === 'todo-label') { 148 | var todoEl = getClosestTodoElement(element); 149 | 150 | event.preventDefault(); 151 | event.stopPropagation(); 152 | 153 | this.showEditor(todoEl); 154 | } 155 | 156 | }, 157 | 158 | /** 159 | * Handles keydown events for the behavior. 160 | * @param {Event} event A DOM-normalized event object. 161 | * @param {HTMLElement} element The nearest HTML element with a data-type 162 | * attribute specified or null if there is none. 163 | * @param {string} elementType The value of data-type for the nearest 164 | * element with that attribute specified or null if there is none. 165 | * @returns {void} 166 | */ 167 | onkeydown: function(event, element, elementType) { 168 | 169 | if (elementType === 'edit-input') { 170 | var todoEl = getClosestTodoElement(element); 171 | 172 | if (event.keyCode === ENTER_KEY) { 173 | this.saveLabel(todoEl); 174 | this.hideEditor(todoEl); 175 | } else if (event.keyCode === ESCAPE_KEY) { 176 | this.hideEditor(todoEl); 177 | } 178 | 179 | } 180 | 181 | }, 182 | 183 | /** 184 | * Displays a input box for the user to edit the label with 185 | * @param {HTMLElement} todoEl The todo element to edit 186 | * @returns {void} 187 | */ 188 | showEditor: function(todoEl) { 189 | var todoId = getClosestTodoId(todoEl), 190 | editInputEl = todoEl.querySelector('.edit'), 191 | title = todosDB.get(todoId).title; // Grab current label 192 | 193 | // Set the edit input value to current label 194 | editInputEl.value = title; 195 | 196 | todoEl.classList.add('editing'); 197 | 198 | // Place user cursor in the input 199 | editInputEl.focus(); 200 | }, 201 | 202 | /** 203 | * Hides the edit input for a given todo 204 | * @param {HTMLElement} todoEl The todo element 205 | * @returns {void} 206 | */ 207 | hideEditor: function(todoEl) { 208 | todoEl.classList.remove('editing'); 209 | }, 210 | 211 | /** 212 | * Saves the value of the edit input and saves it to the db 213 | * @param {HTMLElement} todoEl The todo element to edit 214 | * @returns {void} 215 | */ 216 | saveLabel: function(todoEl) { 217 | var todoId = getClosestTodoId(todoEl), 218 | editInputEl = todoEl.querySelector('.edit'), 219 | newTitle = (editInputEl.value).trim(); 220 | 221 | todoEl.querySelector('label').textContent = newTitle; 222 | todosDB.edit(todoId, newTitle); 223 | } 224 | }; 225 | 226 | }); 227 | -------------------------------------------------------------------------------- /examples/todo/js/modules/footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Footer Module 3 | * @author Box 4 | */ 5 | 6 | /* 7 | * Manages the footer module, including todo counts and filters 8 | */ 9 | Application.addModule('footer', function(context) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var todosDB, 18 | moduleEl; 19 | 20 | //-------------------------------------------------------------------------- 21 | // Public 22 | //-------------------------------------------------------------------------- 23 | 24 | return { 25 | 26 | messages: ['todoadded', 'todoremoved', 'todostatuschange', 'statechanged'], 27 | 28 | /** 29 | * Initializes the module. Caches a data store object to todos 30 | * @returns {void} 31 | */ 32 | init: function() { 33 | todosDB = context.getService('todos-db'); 34 | moduleEl = context.getElement(); 35 | }, 36 | 37 | /** 38 | * Destroys the module. 39 | * @returns {void} 40 | */ 41 | destroy: function() { 42 | moduleEl = null; 43 | todosDB = null; 44 | }, 45 | 46 | /** 47 | * Handles the click event for the module. 48 | * @param {Event} event A DOM-normalized event object. 49 | * @param {HTMLElement} element The nearest HTML element with a data-type 50 | * attribute specified or null if there is none. 51 | * @param {string} elementType The value of data-type for the nearest 52 | * element with that attribute specified or null if there is none. 53 | * @returns {void} 54 | */ 55 | onclick: function(event, element, elementType) { 56 | 57 | // code to be run when a click occurs 58 | if (elementType === 'clear-btn') { 59 | this.clearCompletedTodos(); 60 | } 61 | 62 | }, 63 | 64 | /** 65 | * Handles all messages received for the module. 66 | * @param {string} name The name of the message received. 67 | * @param {*} [data] Additional data sent along with the message. 68 | * @returns {void} 69 | */ 70 | onmessage: function(name, data) { 71 | 72 | switch(name) { 73 | case 'todoadded': 74 | case 'todoremoved': 75 | case 'todostatuschange': 76 | this.updateTodoCounts(); 77 | break; 78 | 79 | case 'statechanged': 80 | this.updateSelectedFilterByUrl(data.url); 81 | break; 82 | } 83 | }, 84 | 85 | /** 86 | * Updates the selected class on the filter links 87 | * @param {string} url The current url 88 | * @returns {void} 89 | */ 90 | updateSelectedFilterByUrl: function(url) { 91 | var linkEls = moduleEl.querySelectorAll('a'); 92 | 93 | for (var i = 0, len = linkEls.length; i < len; i++) { 94 | if (url === linkEls[i].pathname) { 95 | linkEls[i].classList.add('selected'); 96 | } else { 97 | linkEls[i].classList.remove('selected'); 98 | } 99 | } 100 | }, 101 | 102 | 103 | /** 104 | * Updates todo counts based on what is in the todo DB 105 | * @returns {void} 106 | */ 107 | updateTodoCounts: function() { 108 | var todos = todosDB.getList(); 109 | 110 | var completedCount = 0; 111 | for (var i = 0, len = todos.length; i < len; i++) { 112 | if (todos[i].completed) { 113 | completedCount++; 114 | } 115 | } 116 | 117 | var itemsLeft = todos.length - completedCount; 118 | 119 | this.updateItemsLeft(itemsLeft); 120 | this.updateCompletedButton(completedCount); 121 | 122 | }, 123 | 124 | /** 125 | * Updates the displayed count of incomplete tasks 126 | * @param {number} itemsLeft # of incomplete tasks 127 | * @returns {void} 128 | */ 129 | updateItemsLeft: function(itemsLeft) { 130 | var itemText = itemsLeft === 1 ? 'item' : 'items'; 131 | 132 | moduleEl.querySelector('.items-left-counter').textContent = itemsLeft; 133 | moduleEl.querySelector('.items-left-text').textContent = itemText + ' left'; 134 | }, 135 | 136 | /** 137 | * Updates the displayed count of completed tasks and hide/shows the button accordingly 138 | * @param {number} completedCount # of completed tasks 139 | * @returns {void} 140 | */ 141 | updateCompletedButton: function(completedCount) { 142 | if (completedCount > 0) { 143 | moduleEl.querySelector('.completed-count').textContent = completedCount; 144 | moduleEl.classList.add('has-completed-tasks'); 145 | } else { 146 | moduleEl.classList.remove('has-completed-tasks'); 147 | } 148 | }, 149 | 150 | /** 151 | * Removes any todos that have been completed 152 | * @returns {void} 153 | */ 154 | clearCompletedTodos: function() { 155 | todosDB.removeCompleted(); 156 | context.broadcast('todoremoved'); 157 | } 158 | 159 | }; 160 | 161 | }); 162 | -------------------------------------------------------------------------------- /examples/todo/js/modules/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Header Module 3 | * @author Box 4 | */ 5 | 6 | /* 7 | * Handles creation of new todo items 8 | */ 9 | Application.addModule('header', function(context) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var ENTER_KEY = 13; 18 | 19 | var todosDB; 20 | 21 | //-------------------------------------------------------------------------- 22 | // Public 23 | //-------------------------------------------------------------------------- 24 | 25 | return { 26 | 27 | /** 28 | * Initializes the module. Caches a data store object to todos 29 | * @returns {void} 30 | */ 31 | init: function() { 32 | todosDB = context.getService('todos-db'); 33 | }, 34 | 35 | /** 36 | * Destroys the module. 37 | * @returns {void} 38 | */ 39 | destroy: function() { 40 | todosDB = null; 41 | }, 42 | 43 | /** 44 | * Handles the keydown event for the module. 45 | * @param {Event} event A DOM-normalized event object. 46 | * @param {HTMLElement} element The nearest HTML element with a data-type 47 | * attribute specified or null if there is none. 48 | * @param {string} elementType The value of data-type for the nearest 49 | * element with that attribute specified or null if there is none. 50 | * @returns {void} 51 | */ 52 | onkeydown: function(event, element, elementType) { 53 | 54 | // code to be run when a click occurs 55 | if (elementType === 'new-todo-input') { 56 | 57 | if (event.keyCode === ENTER_KEY) { 58 | 59 | var todoTitle = (element.value).trim(); 60 | 61 | if (todoTitle.length) { 62 | var newTodoId = todosDB.add(todoTitle); 63 | 64 | context.broadcast('todoadded', { 65 | id: newTodoId 66 | }); 67 | 68 | // Clear input afterwards 69 | element.value = ''; 70 | } 71 | 72 | event.preventDefault(); 73 | event.stopPropagation(); 74 | } 75 | 76 | } 77 | 78 | } 79 | }; 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /examples/todo/js/modules/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Todo List 3 | * @author Box 4 | */ 5 | 6 | /* 7 | * Manages the todo list which includes adding/removing/checking items 8 | */ 9 | Application.addModule('list', function(context) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | var todosDB, 17 | moduleEl, 18 | listEl, 19 | filter; 20 | 21 | /** 22 | * Returns true if all todos in the list are complete 23 | * @returns {boolean} 24 | * @private 25 | */ 26 | function isListCompleted() { 27 | var todos = todosDB.getList(); 28 | var len = todos.length; 29 | var isComplete = len > 0; 30 | for (var i = 0; i < len; i++) { 31 | if (!todos[i].completed) { 32 | isComplete = false; 33 | break; 34 | } 35 | } 36 | return isComplete; 37 | } 38 | 39 | /** 40 | * Sets the todo filter based on URL 41 | * @returns {void} 42 | * @private 43 | */ 44 | function setFilterByUrl(url) { 45 | if (url.indexOf('/active') > -1) { 46 | filter = 'incomplete'; 47 | } else if (url.indexOf('/completed') > -1) { 48 | filter = 'complete'; 49 | } else { 50 | filter = ''; 51 | } 52 | } 53 | 54 | //-------------------------------------------------------------------------- 55 | // Public 56 | //-------------------------------------------------------------------------- 57 | 58 | return { 59 | /** 60 | * The behaviors that this module uses. 61 | * @type String[] 62 | */ 63 | behaviors: ['todo'], 64 | 65 | /** 66 | * The messages that this modules listens for. 67 | * @type String[] 68 | */ 69 | messages: ['todoadded', 'todoremoved', 'todostatuschange', 'statechanged'], 70 | 71 | /** 72 | * Initializes the module. Caches a data store object to todos 73 | * @returns {void} 74 | */ 75 | init: function() { 76 | todosDB = context.getService('todos-db'); 77 | 78 | moduleEl = context.getElement(); 79 | listEl = moduleEl.querySelector('#todo-list'); 80 | }, 81 | 82 | /** 83 | * Destroys the module. 84 | * @returns {void} 85 | */ 86 | destroy: function() { 87 | listEl = null; 88 | moduleEl = null; 89 | todosDB = null; 90 | }, 91 | 92 | /** 93 | * Handles all click events for the module. 94 | * @param {Event} event A DOM-normalized event object. 95 | * @param {HTMLElement} element The nearest HTML element with a data-type 96 | * attribute specified or null if there is none. 97 | * @param {string} elementType The value of data-type for the nearest 98 | * element with that attribute specified or null if there is none. 99 | * @returns {void} 100 | */ 101 | onchange: function(event, element, elementType) { 102 | 103 | if (elementType === 'select-all-checkbox') { 104 | var shouldMarkAsComplete = element.checked; 105 | 106 | if (shouldMarkAsComplete) { 107 | todosDB.markAllAsComplete(); 108 | } else { 109 | todosDB.markAllAsIncomplete(); 110 | } 111 | 112 | this.renderList(); 113 | 114 | context.broadcast('todostatuschange'); 115 | } 116 | 117 | }, 118 | 119 | /** 120 | * Handles all messages received for the module. 121 | * @param {string} name The name of the message received. 122 | * @param {*} [data] Additional data sent along with the message. 123 | * @returns {void} 124 | */ 125 | onmessage: function(name, data) { 126 | 127 | switch(name) { 128 | case 'todoadded': 129 | case 'todoremoved': 130 | this.renderList(); 131 | this.updateSelectAllCheckbox(); 132 | break; 133 | 134 | case 'todostatuschange': 135 | this.updateSelectAllCheckbox(); 136 | break; 137 | 138 | case 'statechanged': 139 | setFilterByUrl(data.url); 140 | this.renderList(); 141 | break; 142 | } 143 | 144 | }, 145 | 146 | /** 147 | * Creates a list item for a todo based off of a template 148 | * @param {number} id The id of the todo 149 | * @param {string} title The todo label 150 | * @param {boolean} isCompleted Is todo complete 151 | * @returns {Node} 152 | */ 153 | createTodo: function(id, title, isCompleted) { 154 | var todoTemplateEl = moduleEl.querySelector('.todo-template-container li'), 155 | newTodoEl = todoTemplateEl.cloneNode(true); 156 | 157 | // Set the label of the todo 158 | newTodoEl.querySelector('label').textContent = title; 159 | newTodoEl.setAttribute('data-todo-id', id); 160 | if (isCompleted) { 161 | newTodoEl.classList.add('completed'); 162 | newTodoEl.querySelector('input[type="checkbox"]').checked = true; 163 | } 164 | 165 | return newTodoEl; 166 | }, 167 | 168 | /** 169 | * Appends a new todo list element to the todo-list 170 | * @param {number} id The id of the todo 171 | * @param {string} title The todo label 172 | * @param {boolean} isCompleted Is todo complete 173 | * @returns {void} 174 | */ 175 | addTodoItem: function(id, title, isCompleted) { 176 | listEl.appendChild(this.createTodo(id, title, isCompleted)); 177 | }, 178 | 179 | /** 180 | * Updates the 'checked' status of the select-all checkbox based on the status of the list 181 | * @returns {void} 182 | */ 183 | updateSelectAllCheckbox: function() { 184 | var selectAllCheckboxEl = moduleEl.querySelector('#toggle-all'); 185 | selectAllCheckboxEl.checked = isListCompleted(); 186 | }, 187 | 188 | /** 189 | * Removes all todo elements from the list (not necessarily from the db) 190 | * @returns {void} 191 | */ 192 | clearList: function() { 193 | var todoListEl = moduleEl.querySelector('#todo-list'); 194 | while (todoListEl.hasChildNodes()) { 195 | todoListEl.removeChild(todoListEl.lastChild); 196 | } 197 | }, 198 | 199 | /** 200 | * Renders all todos in the todos db 201 | * @returns {void} 202 | */ 203 | renderList: function() { 204 | // Clear the todo list first 205 | this.clearList(); 206 | 207 | var todos = todosDB.getList(), 208 | todo; 209 | 210 | // Render todos - factor in the current filter as well 211 | for (var i = 0, len = todos.length; i < len; i++) { 212 | todo = todos[i]; 213 | 214 | if (!filter 215 | || (filter === 'incomplete' && !todo.completed) 216 | || (filter === 'complete' && todo.completed)) { 217 | this.addTodoItem(todo.id, todo.title, todo.completed); 218 | 219 | } 220 | } 221 | } 222 | }; 223 | 224 | }); 225 | -------------------------------------------------------------------------------- /examples/todo/js/modules/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Page Module 3 | * @author Box 4 | */ 5 | 6 | /* 7 | * This module only handles routing on the page (via anchor links) 8 | */ 9 | Application.addModule('page', function(context) { 10 | 11 | 'use strict'; 12 | 13 | //-------------------------------------------------------------------------- 14 | // Private 15 | //-------------------------------------------------------------------------- 16 | 17 | var routerService; 18 | 19 | //-------------------------------------------------------------------------- 20 | // Public 21 | //-------------------------------------------------------------------------- 22 | 23 | return { 24 | 25 | /** 26 | * Initializes the module. Caches a data store object to todos 27 | * @returns {void} 28 | */ 29 | init: function() { 30 | var baseUrl = context.getGlobal('location').pathname; 31 | 32 | routerService = context.getService('router'); 33 | routerService.init([ 34 | baseUrl, 35 | baseUrl + 'active', 36 | baseUrl + 'completed' 37 | ]); 38 | }, 39 | 40 | /** 41 | * Destroys the module. 42 | * @returns {void} 43 | */ 44 | destroy: function() { 45 | routerService.destroy(); 46 | } 47 | 48 | }; 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /examples/todo/js/services/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A page routing service 3 | * @author Box 4 | */ 5 | 6 | /* 7 | * A router for our objects 8 | */ 9 | Application.addService('router', function(application) { 10 | 11 | 'use strict'; 12 | 13 | var history = application.getGlobal('history'); 14 | var pageRoutes; 15 | 16 | var regexRoutes = [], 17 | prevUrl; 18 | 19 | /** 20 | * Parses the user-friendly declared routes at the top of the application, 21 | * these could be init'ed or passed in from another context to help extract 22 | * navigation for T3 from the framework. Populates the regexRoutes variable 23 | * that is local to the service. 24 | * Regexs and parsing borrowed from Backbone's router since they did it right 25 | * and not inclined to make a new router syntax unless we have a reason to. 26 | * @returns {void} 27 | */ 28 | function parseRoutes() { 29 | // Regexs to convert a route (/file/:fileId) into a regex 30 | var optionalParam = /\((.*?)\)/g, 31 | namedParam = /(\(\?)?:\w+/g, 32 | splatParam = /\*\w+/g, 33 | escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g, 34 | namedParamCallback = function(match, optional) { 35 | return optional ? match : '([^\/]+)'; 36 | }, 37 | route, 38 | regexRoute; 39 | 40 | for (var i = 0, len = pageRoutes.length; i < len; i++) { 41 | route = pageRoutes[i]; 42 | regexRoute = route.replace(escapeRegExp, '\\$&') 43 | .replace(optionalParam, '(?:$1)?') 44 | .replace(namedParam, namedParamCallback) 45 | .replace(splatParam, '(.*?)'); 46 | regexRoutes.push(new RegExp('^' + regexRoute + '$')); 47 | } 48 | } 49 | 50 | /** 51 | * Finds the regex route that matches the current fragment and returns 52 | * the matched route. This could eventually map back to the route it was 53 | * created from to return a smarter object (ie /file/:fileId returning {fileId: 42}) 54 | * @param {string} fragment represents the current "URL" the user is getting to 55 | * @return {Object} 56 | */ 57 | function matchGlob(fragment) { 58 | var regexRoute, 59 | globMatches; 60 | 61 | for (var idx in regexRoutes) { 62 | regexRoute = regexRoutes[idx]; 63 | 64 | if (regexRoute.test(fragment)) { 65 | globMatches = fragment.match(regexRoute); 66 | } 67 | } 68 | 69 | return globMatches; 70 | } 71 | 72 | /** 73 | * Gets the fragment and broadcasts the previous URL and the current URL of 74 | * the page, ideally we wouldn't have a dependency on the previous URL... 75 | * @param {string} fragment the current "URL" the user is getting to 76 | * @returns {void} 77 | */ 78 | function broadcastStateChanged(fragment) { 79 | var globMatches = matchGlob(fragment); 80 | 81 | if (!!globMatches) { 82 | application.broadcast('statechanged', { 83 | url: fragment, 84 | prevUrl: prevUrl, 85 | params: globMatches.splice(1) // Get everything after the URL 86 | }); 87 | } 88 | } 89 | 90 | return { 91 | /** 92 | * The magical method the application code will call to navigate around, 93 | * high level it pushes the state, gets the templates from server, puts 94 | * them on the page and broadcasts the statechanged. 95 | * @param {Object} state the state associated with the URL 96 | * @param {string} title the title of the page 97 | * @param {string} fragment the current "URL" the user is getting to 98 | * @returns {void} 99 | */ 100 | route: function(state, title, fragment) { 101 | prevUrl = history.state.hash; 102 | 103 | // First push the state 104 | history.pushState(state, title, fragment); 105 | 106 | // Then make the AJAX request 107 | broadcastStateChanged(fragment); 108 | }, 109 | 110 | /** 111 | * Initialization that has to be done when the navigation service is required 112 | * by application code, sets up routes (which could be done later) and binds 113 | * to the popstate/hashchange events (which have to happen now). 114 | * @private 115 | * @param {string[]} routes An array of special route strings 116 | * @returns {void} 117 | */ 118 | init: function(inputRoutes) { 119 | pageRoutes = inputRoutes || {}; 120 | 121 | // Turn the routes globs into regular expressions... 122 | parseRoutes(); 123 | 124 | var me = this; 125 | 126 | // Bind the document click listener to all anchor tags 127 | $(document).on('click.router', 'a', function(e) { 128 | // Append the query string to the end of the pathname 129 | var url = e.target.pathname + e.target.search; 130 | 131 | // Stop the event here and we can handle it 132 | e.preventDefault(); 133 | e.stopPropagation(); 134 | 135 | // Check the hash and the URL and make sure they aren't the same 136 | // and then navigate (in the case of an anchors href='#') 137 | if (url !== history.state.hash) { 138 | me.route({}, '', url); 139 | } 140 | }); 141 | 142 | window.onpopupstate = function() { 143 | var state = history.state, 144 | url = state.hash; 145 | 146 | me.route({}, '', url); 147 | }; 148 | 149 | history.pushState({}, '', application.getGlobal('location').pathname); 150 | }, 151 | 152 | /** 153 | * Unbinds the event handlers on the DOM 154 | * @returns {void} 155 | */ 156 | destroy: function() { 157 | $(document).off('click.router', 'a'); 158 | } 159 | }; 160 | }); 161 | 162 | -------------------------------------------------------------------------------- /examples/todo/js/services/todos-db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A todo object manager with built-in functionality 3 | * @author Box 4 | */ 5 | 6 | /** 7 | * Todo Object 8 | * @typedef {Object} Todo 9 | * @property {number} id A unique identifier 10 | * @property {string} title A label for the todo 11 | * @property {boolean} completed Is the task complete? 12 | */ 13 | 14 | /* 15 | * A todo object manager with built-in functionality 16 | */ 17 | Application.addService('todos-db', function() { 18 | 19 | 'use strict'; 20 | 21 | //-------------------------------------------------------------------------- 22 | // Private 23 | //-------------------------------------------------------------------------- 24 | /** @type {Object} */ 25 | var todos = {}; 26 | 27 | /** @type {number} */ 28 | var counter = 0; 29 | 30 | //-------------------------------------------------------------------------- 31 | // Public 32 | //-------------------------------------------------------------------------- 33 | 34 | return { 35 | 36 | /** 37 | * Returns a Todo by id 38 | * @returns {Todo} 39 | */ 40 | get: function(id) { 41 | return todos[id] || null; 42 | }, 43 | 44 | /** 45 | * Returns list of all todos 46 | * @returns {Todo[]} List of todos 47 | */ 48 | getList: function() { 49 | var todoList = []; 50 | 51 | Object.keys(todos).forEach(function(id) { 52 | todoList.push(todos[id]); 53 | }); 54 | 55 | return todoList; 56 | }, 57 | 58 | /** 59 | * Marks all todos as complete 60 | * @returns {void} 61 | */ 62 | markAllAsComplete: function() { 63 | var me = this; 64 | Object.keys(todos).forEach(function(id) { 65 | me.markAsComplete(id); 66 | }); 67 | }, 68 | 69 | /** 70 | * Marks a todo as completed 71 | * @param {number} id The id of the todo 72 | * @returns {void} 73 | */ 74 | markAsComplete: function(id) { 75 | if (todos[id]) { 76 | todos[id].completed = true; 77 | } 78 | }, 79 | 80 | /** 81 | * Marks all todos as incomplete 82 | * @returns {void} 83 | */ 84 | markAllAsIncomplete: function() { 85 | var me = this; 86 | Object.keys(todos).forEach(function(id) { 87 | me.markAsIncomplete(id); 88 | }); 89 | }, 90 | 91 | /** 92 | * Marks a todo as incomplete 93 | * @param {number} id The id of the todo 94 | * @returns {void} 95 | */ 96 | markAsIncomplete: function(id) { 97 | if (todos[id]) { 98 | todos[id].completed = false; 99 | } 100 | }, 101 | 102 | /** 103 | * Removes all completed tasks 104 | * @returns {void} 105 | */ 106 | removeCompleted: function() { 107 | var me = this; 108 | Object.keys(todos).forEach(function(id) { 109 | if (todos[id].completed) { 110 | me.remove(id); 111 | } 112 | }); 113 | }, 114 | 115 | /** 116 | * Adds a todo 117 | * @param {string} title The label of the todo 118 | * @returns {number} The id of the new todo 119 | */ 120 | add: function(title) { 121 | var todoId = counter++; 122 | todos[todoId] = { 123 | id: todoId, 124 | title: title, 125 | completed: false 126 | }; 127 | return todoId; 128 | }, 129 | 130 | /** 131 | * Edits a todo label 132 | * @param {number} id The unique identifier of the todo 133 | * @param {string} title The new label of the todo 134 | * @returns {void} 135 | */ 136 | edit: function(id, title) { 137 | if (todos[id]) { 138 | todos[id].title = title; 139 | } 140 | }, 141 | 142 | /** 143 | * Removes a todo by id 144 | * @param {number} id identifier of Todo to remove 145 | * @returns {void} 146 | */ 147 | remove: function(id) { 148 | if (todos[id]) { 149 | delete todos[id]; 150 | } 151 | } 152 | }; 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /lib/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "brace-style": 2, 4 | "func-style": [2, "declaration"], 5 | "strict": [2, "function"], 6 | "guard-for-in": 2, 7 | "no-floating-decimal": 2, 8 | "no-underscore-dangle": 0, 9 | "no-nested-ternary": 2, 10 | "quotes": [2, "single"], 11 | "radix": 2, 12 | "wrap-iife": 2, 13 | "space-after-keywords": 2, 14 | "valid-jsdoc": [2, { 15 | "prefer": { 16 | "return": "returns" 17 | } 18 | }], 19 | // We allow unused args 20 | "no-unused-vars": [2, {"args": "none"}] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/application-stub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Fake application to use during testing 3 | * @author Box 4 | */ 5 | 6 | (function() { 7 | 8 | 'use strict'; 9 | 10 | /* 11 | * When testing actual Application, it should be included after to overwrite this stub. 12 | */ 13 | Box.Application = (function() { 14 | 15 | var services = {}, 16 | modules = {}, 17 | behaviors = {}; 18 | 19 | return { 20 | 21 | /** 22 | * Resets the application stub back to a clean state. Will also remove pre-registered components. 23 | * @returns {Box.Application} The application object. 24 | */ 25 | reset: function() { 26 | services = {}; 27 | modules = {}; 28 | behaviors = {}; 29 | return this; 30 | }, 31 | 32 | /** 33 | * Registers a service to the application stub 34 | * @param {string} serviceName The name of the service 35 | * @param {Function} creator The service creator function 36 | * @returns {Box.Application} The application object. 37 | */ 38 | addService: function(serviceName, creator) { 39 | services[serviceName] = { 40 | creator: creator 41 | }; 42 | return this; 43 | }, 44 | 45 | /** 46 | * Registers a module to the application stub 47 | * @param {string} moduleName The name of the module 48 | * @param {Function} creator The behavior creator function 49 | * @returns {Box.Application} The application object. 50 | */ 51 | addModule: function(moduleName, creator) { 52 | modules[moduleName] = { 53 | creator: creator 54 | }; 55 | return this; 56 | }, 57 | 58 | /** 59 | * Registers a behavior to the application stub 60 | * @param {string} behaviorName The name of the behavior 61 | * @param {Function} creator The behavior creator function 62 | * @returns {Box.Application} The application object. 63 | */ 64 | addBehavior: function(behaviorName, creator) { 65 | behaviors[behaviorName] = { 66 | creator: creator 67 | }; 68 | return this; 69 | }, 70 | 71 | /** 72 | * Checks if a service exists 73 | * @param {string} serviceName The name of the service to check. 74 | * @returns {boolean} True, if service exist. False, otherwise. 75 | */ 76 | hasService: function(serviceName) { 77 | return services.hasOwnProperty(serviceName); 78 | }, 79 | 80 | /** 81 | * Will create a new instance of a service with the given application context 82 | * @param {string} serviceName The name of the service being created 83 | * @param {Object} application The application context object (usually a TestServiceProvider) 84 | * @returns {?Object} The service object 85 | */ 86 | getServiceForTest: function(serviceName, application) { 87 | var serviceData = services[serviceName]; 88 | if (serviceData) { 89 | return services[serviceName].creator(application); 90 | } 91 | return null; 92 | }, 93 | 94 | /** 95 | * Will create a new instance of a module with a given context 96 | * @param {string} moduleName The name of the module being created 97 | * @param {Object} context The context object (usually a TestServiceProvider) 98 | * @returns {?Object} The module object 99 | */ 100 | getModuleForTest: function(moduleName, context) { 101 | var module = modules[moduleName].creator(context); 102 | 103 | if (!context.getElement) { 104 | // Add in a default getElement function that matches the first module element 105 | // Developer should stub this out if there are more than one instance of this module 106 | context.getElement = function() { 107 | return document.querySelector('[data-module="' + moduleName + '"]'); 108 | }; 109 | } 110 | return module; 111 | }, 112 | 113 | /** 114 | * Will create a new instance of a behavior with a given context 115 | * @param {string} behaviorName The name of the behavior being created 116 | * @param {Object} context The context object (usually a TestServiceProvider) 117 | * @returns {?Object} The behavior object 118 | */ 119 | getBehaviorForTest: function(behaviorName, context) { 120 | var behaviorData = behaviors[behaviorName]; 121 | if (behaviorData) { 122 | // getElement on behaviors must be stubbed 123 | if (!context.getElement) { 124 | context.getElement = function() { 125 | throw new Error('You must stub `getElement` for behaviors.'); 126 | }; 127 | } 128 | return behaviors[behaviorName].creator(context); 129 | } 130 | return null; 131 | } 132 | 133 | }; 134 | 135 | }()); 136 | 137 | }()); 138 | -------------------------------------------------------------------------------- /lib/box.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Base namespaces for Box JavaScript. 3 | * @author Box 4 | */ 5 | 6 | /* eslint-disable no-unused-vars */ 7 | 8 | /** 9 | * The one global object for Box JavaScript. 10 | * @namespace 11 | */ 12 | var Box = {}; 13 | /* eslint-enable no-unused-vars */ 14 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Contains the Context type which is used by modules to interact 3 | * with the environment. 4 | * @author Box 5 | */ 6 | 7 | Box.Context = (function() { 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * The object type that modules use to interact with the environment. Used 13 | * exclusively within Box.Application, but exposed publicly for easy testing. 14 | * @param {Box.Application} application The application object to wrap. 15 | * @param {HTMLElement} element Module's DOM element 16 | * @constructor 17 | */ 18 | function Context(application, element) { 19 | this.application = application; 20 | this.element = element; 21 | } 22 | 23 | //------------------------------------------------------------------------- 24 | // Passthrough Methods 25 | //------------------------------------------------------------------------- 26 | 27 | Context.prototype = { 28 | constructor: Context, 29 | 30 | /** 31 | * Passthrough method to application that broadcasts messages. 32 | * @param {string} name Name of the message event 33 | * @param {*} [data] Custom parameters for the message 34 | * @returns {void} 35 | */ 36 | broadcast: function(name, data) { 37 | this.application.broadcast(name, data); 38 | }, 39 | 40 | /** 41 | * Passthrough method to application that retrieves services. 42 | * @param {string} serviceName The name of the service to retrieve. 43 | * @returns {Object|null} An object if the service is found or null if not. 44 | */ 45 | getService: function(serviceName) { 46 | return this.application.getService(serviceName); 47 | }, 48 | 49 | /** 50 | * Checks if a service exists 51 | * @param {string} serviceName The name of the service to check. 52 | * @returns {boolean} True, if service exist. False, otherwise. 53 | */ 54 | hasService: function(serviceName) { 55 | return this.application.hasService(serviceName); 56 | }, 57 | 58 | /** 59 | * Returns any configuration information that was output into the page 60 | * for this instance of the module. 61 | * @param {string} [name] Specific config parameter 62 | * @returns {*} config value or the entire configuration JSON object 63 | * if no name is specified (null if either not found) 64 | */ 65 | getConfig: function(name) { 66 | return this.application.getModuleConfig(this.element, name); 67 | }, 68 | 69 | /** 70 | * Returns a global variable 71 | * @param {string} name Specific global var name 72 | * @returns {*} returns the window-scope variable matching the name, null otherwise 73 | */ 74 | getGlobal: function(name) { 75 | return this.application.getGlobal(name); 76 | }, 77 | 78 | /** 79 | * Returns global configuration data 80 | * @param {string} [name] Specific config parameter 81 | * @returns {*} config value or the entire configuration JSON object 82 | * if no name is specified (null if either not found) 83 | */ 84 | getGlobalConfig: function(name) { 85 | return this.application.getGlobalConfig(name); 86 | }, 87 | 88 | /** 89 | * Passthrough method that signals that an error has occurred. If in development mode, an error 90 | * is thrown. If in production mode, an event is fired. 91 | * @param {Error} [exception] The exception object to use. 92 | * @returns {void} 93 | */ 94 | reportError: function(exception) { 95 | this.application.reportError(exception); 96 | }, 97 | 98 | //------------------------------------------------------------------------- 99 | // Service Shortcuts 100 | //------------------------------------------------------------------------- 101 | 102 | /** 103 | * Returns the element that represents the module. 104 | * @returns {HTMLElement} The element representing the module. 105 | */ 106 | getElement: function() { 107 | return this.element; 108 | } 109 | 110 | }; 111 | 112 | return Context; 113 | 114 | }()); 115 | -------------------------------------------------------------------------------- /lib/dom-event-delegate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview An object that encapsulates event delegation wireup for a 3 | * DOM element. 4 | * @author Box 5 | */ 6 | 7 | Box.DOMEventDelegate = (function() { 8 | 9 | 'use strict'; 10 | 11 | // Supported events for modules. Only events that bubble properly can be used in T3. 12 | var DEFAULT_EVENT_TYPES = ['click', 'mouseover', 'mouseout', 'mousedown', 'mouseup', 13 | 'mouseenter', 'mouseleave', 'mousemove', 'keydown', 'keyup', 'submit', 'change', 14 | 'contextmenu', 'dblclick', 'input', 'focusin', 'focusout']; 15 | 16 | 17 | /** 18 | * Determines if a given element represents a module. 19 | * @param {HTMLElement} element The element to check. 20 | * @returns {boolean} True if the element represents a module, false if not. 21 | * @private 22 | */ 23 | function isModuleElement(element) { 24 | return element && element.hasAttribute('data-module'); 25 | } 26 | 27 | /** 28 | * Determines if a given element represents a T3 type. 29 | * @param {HTMLElement} element The element to check. 30 | * @returns {boolean} True if the element represents a T3 type, false if not. 31 | * @private 32 | */ 33 | function isTypeElement(element) { 34 | return element && element.hasAttribute('data-type'); 35 | } 36 | 37 | /** 38 | * Finds the closest ancestor that of an element that has a data-type 39 | * attribute. 40 | * @param {HTMLElement} element The element to start searching from. 41 | * @returns {HTMLElement} The matching element or null if not found. 42 | */ 43 | function getNearestTypeElement(element) { 44 | var found = false; 45 | var moduleBoundaryReached = false; 46 | 47 | // We need to check for the existence of 'element' since occasionally we call this on a detached element node. 48 | // For example: 49 | // 1. event handlers like mouseout may sometimes detach nodes from the DOM 50 | // 2. event handlers like mouseleave will still fire on the detached node 51 | // Checking existence of element.parentNode ensures the element is a valid HTML Element 52 | while (!found && element && element.parentNode && !moduleBoundaryReached) { 53 | found = isTypeElement(element); 54 | moduleBoundaryReached = isModuleElement(element); 55 | 56 | if (!found) { 57 | element = element.parentNode; 58 | } 59 | 60 | } 61 | 62 | return found ? element : null; 63 | } 64 | 65 | /** 66 | * Iterates over each supported event type that is also in the handler, applying 67 | * a callback function. This is used to more easily attach/detach all events. 68 | * @param {string[]} eventTypes A list of event types to iterate over 69 | * @param {Object} handler An object with onclick, onmouseover, etc. methods. 70 | * @param {Function} callback The function to call on each event type. 71 | * @param {Object} [thisValue] The value of "this" inside the callback. 72 | * @returns {void} 73 | * @private 74 | */ 75 | function forEachEventType(eventTypes, handler, callback, thisValue) { 76 | 77 | var i, 78 | type; 79 | 80 | for (i = 0; i < eventTypes.length; i++) { 81 | type = eventTypes[i]; 82 | 83 | // only call the callback if the event is on the handler 84 | if (handler['on' + type]) { 85 | callback.call(thisValue, type); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * An object that manages events within a single DOM element. 92 | * @param {HTMLElement} element The DOM element to handle events for. 93 | * @param {Object} handler An object containing event handlers such as "onclick". 94 | * @param {string[]} [eventTypes] A list of event types to handle (events must bubble). Defaults to a common set of events. 95 | * @constructor 96 | */ 97 | function DOMEventDelegate(element, handler, eventTypes) { 98 | 99 | /** 100 | * The DOM element that this object is handling events for. 101 | * @type {HTMLElement} 102 | */ 103 | this.element = element; 104 | 105 | /** 106 | * Object on which event handlers are available. 107 | * @type {Object} 108 | * @private 109 | */ 110 | this._handler = handler; 111 | 112 | /** 113 | * List of event types to handle (make sure these events bubble!) 114 | * @type {string[]} 115 | * @private 116 | */ 117 | this._eventTypes = eventTypes || DEFAULT_EVENT_TYPES; 118 | 119 | /** 120 | * Tracks event handlers whose this-value is bound to the correct 121 | * object. 122 | * @type {Object} 123 | * @private 124 | */ 125 | this._boundHandler = {}; 126 | 127 | /** 128 | * Indicates if events have been attached. 129 | * @type {boolean} 130 | * @private 131 | */ 132 | this._attached = false; 133 | } 134 | 135 | 136 | DOMEventDelegate.prototype = { 137 | 138 | // restore constructor 139 | constructor: DOMEventDelegate, 140 | 141 | _handleEvent: function(event) { 142 | var targetElement = getNearestTypeElement(event.target), 143 | elementType = targetElement ? targetElement.getAttribute('data-type') : ''; 144 | 145 | this._handler['on' + event.type](event, targetElement, elementType); 146 | }, 147 | 148 | /** 149 | * Attaches all event handlers for the DOM element. 150 | * @returns {void} 151 | */ 152 | attachEvents: function() { 153 | if (!this._attached) { 154 | 155 | forEachEventType(this._eventTypes, this._handler, function(eventType) { 156 | var that = this; 157 | 158 | function handleEvent() { 159 | that._handleEvent.apply(that, arguments); 160 | } 161 | 162 | Box.DOM.on(this.element, eventType, handleEvent); 163 | 164 | this._boundHandler[eventType] = handleEvent; 165 | }, this); 166 | 167 | this._attached = true; 168 | } 169 | }, 170 | 171 | /** 172 | * Detaches all event handlers for the DOM element. 173 | * @returns {void} 174 | */ 175 | detachEvents: function() { 176 | forEachEventType(this._eventTypes, this._handler, function(eventType) { 177 | Box.DOM.off(this.element, eventType, this._boundHandler[eventType]); 178 | }, this); 179 | } 180 | }; 181 | 182 | return DOMEventDelegate; 183 | }()); 184 | 185 | -------------------------------------------------------------------------------- /lib/dom-jquery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview DOM abstraction to use jquery to add and remove event listeners 3 | * in T3 4 | * @author jdivock 5 | */ 6 | 7 | /* eslint-env jquery */ 8 | 9 | Box.JQueryDOM = (function() { 10 | 'use strict'; 11 | 12 | return { 13 | 14 | type: 'jquery', 15 | 16 | /** 17 | * Returns the first element that is a descendant of the element 18 | * on which it is invoked that matches the specified group of selectors. 19 | * @param {HTMLElement} root parent element to query off of 20 | * @param {string} selector query string to match on 21 | * 22 | * @returns {HTMLElement} first element found matching query 23 | */ 24 | query: function(root, selector) { 25 | // Aligning with native which returns null if not found 26 | return jQuery(root).find(selector)[0] || null; 27 | }, 28 | 29 | /** 30 | * Returns a non-live NodeList of all elements descended from the 31 | * element on which it is invoked that match the specified group of CSS selectors. 32 | * @param {HTMLElement} root parent element to query off of 33 | * @param {string} selector query string to match on 34 | * 35 | * @returns {Array} elements found matching query 36 | */ 37 | queryAll: function(root, selector) { 38 | return jQuery.makeArray(jQuery(root).find(selector)); 39 | }, 40 | 41 | /** 42 | * Adds event listener to element via jquery 43 | * @param {HTMLElement} element Target to attach listener to 44 | * @param {string} type Name of the action to listen for 45 | * @param {function} listener Function to be executed on action 46 | * 47 | * @returns {void} 48 | */ 49 | on: function(element, type, listener) { 50 | jQuery(element).on(type, listener); 51 | }, 52 | 53 | /** 54 | * Removes event listener to element via jquery 55 | * @param {HTMLElement} element Target to remove listener from 56 | * @param {string} type Name of the action remove listener from 57 | * @param {function} listener Function to be removed from action 58 | * 59 | * @returns {void} 60 | */ 61 | off: function(element, type, listener) { 62 | jQuery(element).off(type, listener); 63 | } 64 | }; 65 | }()); 66 | 67 | Box.DOM = Box.JQueryDOM; 68 | -------------------------------------------------------------------------------- /lib/dom-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview DOM abstraction to use native browser functionality to add and remove event listeners 3 | * in T3 4 | * @author jdivock 5 | */ 6 | 7 | 8 | Box.NativeDOM = (function(){ 9 | 'use strict'; 10 | 11 | return { 12 | 13 | type: 'native', 14 | 15 | /** 16 | * Returns the first element that is a descendant of the element 17 | * on which it is invoked that matches the specified group of selectors. 18 | * @param {HTMLElement} root parent element to query off of 19 | * @param {string} selector query string to match on 20 | * 21 | * @returns {HTMLElement} first element found matching query 22 | */ 23 | query: function(root, selector){ 24 | return root.querySelector(selector); 25 | }, 26 | 27 | /** 28 | * Returns a non-live NodeList of all elements descended from the 29 | * element on which it is invoked that match the specified group of CSS selectors. 30 | * @param {HTMLElement} root parent element to query off of 31 | * @param {string} selector query string to match on 32 | * 33 | * @returns {Array} elements found matching query 34 | */ 35 | queryAll: function(root, selector){ 36 | return root.querySelectorAll(selector); 37 | }, 38 | 39 | /** 40 | * Adds event listener to element using native event listener 41 | * @param {HTMLElement} element Target to attach listener to 42 | * @param {string} type Name of the action to listen for 43 | * @param {function} listener Function to be executed on action 44 | * 45 | * @returns {void} 46 | */ 47 | on: function(element, type, listener) { 48 | element.addEventListener(type, listener, false); 49 | }, 50 | 51 | /** 52 | * Removes event listener to element using native event listener functions 53 | * @param {HTMLElement} element Target to remove listener from 54 | * @param {string} type Name of the action remove listener from 55 | * @param {function} listener Function to be removed from action 56 | * 57 | * @returns {void} 58 | */ 59 | off: function(element, type, listener) { 60 | element.removeEventListener(type, listener, false); 61 | } 62 | }; 63 | }()); 64 | 65 | Box.DOM = Box.NativeDOM; 66 | -------------------------------------------------------------------------------- /lib/event-target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Definition of a custom event type. This is used as a utility 3 | * throughout the framework whenever custom events are used. It is intended to 4 | * be inherited from, either through the prototype or via mixin. 5 | * @author Box 6 | */ 7 | 8 | Box.EventTarget = (function() { 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * An object that is capable of generating custom events and also 14 | * executing handlers for events when they occur. 15 | * @constructor 16 | */ 17 | function EventTarget() { 18 | 19 | /** 20 | * Map of events to handlers. The keys in the object are the event names. 21 | * The values in the object are arrays of event handler functions. 22 | * @type {Object} 23 | * @private 24 | */ 25 | this._handlers = {}; 26 | } 27 | 28 | EventTarget.prototype = { 29 | 30 | // restore constructor 31 | constructor: EventTarget, 32 | 33 | /** 34 | * Adds a new event handler for a particular type of event. 35 | * @param {string} type The name of the event to listen for. 36 | * @param {Function} handler The function to call when the event occurs. 37 | * @returns {void} 38 | */ 39 | on: function(type, handler) { 40 | 41 | var handlers = this._handlers[type], 42 | i, 43 | len; 44 | 45 | if (typeof handlers === 'undefined') { 46 | handlers = this._handlers[type] = []; 47 | } 48 | 49 | for (i = 0, len = handlers.length; i < len; i++) { 50 | if (handlers[i] === handler) { 51 | // prevent duplicate handlers 52 | return; 53 | } 54 | } 55 | 56 | handlers.push(handler); 57 | }, 58 | 59 | /** 60 | * Fires an event with the given name and data. 61 | * @param {string} type The type of event to fire. 62 | * @param {Object} [data] An object with properties that should end up on 63 | * the event object for the given event. 64 | * @returns {void} 65 | */ 66 | fire: function(type, data) { 67 | 68 | var handlers, 69 | i, 70 | len, 71 | event = { 72 | type: type, 73 | data: data 74 | }; 75 | 76 | // if there are handlers for the event, call them in order 77 | handlers = this._handlers[event.type]; 78 | if (handlers instanceof Array) { 79 | // @NOTE: do a concat() here to create a copy of the handlers array, 80 | // so that if another handler is removed of the same type, it doesn't 81 | // interfere with the handlers array during this loop 82 | handlers = handlers.concat(); 83 | for (i = 0, len = handlers.length; i < len; i++) { 84 | handlers[i].call(this, event); 85 | } 86 | } 87 | }, 88 | 89 | /** 90 | * Removes an event handler from a given event. 91 | * @param {string} type The name of the event to remove from. 92 | * @param {Function} handler The function to remove as a handler. 93 | * @returns {void} 94 | */ 95 | off: function(type, handler) { 96 | 97 | var handlers = this._handlers[type], 98 | i, 99 | len; 100 | 101 | if (handlers instanceof Array) { 102 | for (i = 0, len = handlers.length; i < len; i++) { 103 | if (handlers[i] === handler) { 104 | handlers.splice(i, 1); 105 | break; 106 | } 107 | } 108 | } 109 | } 110 | }; 111 | 112 | return EventTarget; 113 | 114 | }()); 115 | -------------------------------------------------------------------------------- /lib/test-service-provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A service provider that also contains a few pre-stubbed functions 3 | * @author Box 4 | */ 5 | 6 | (function() { 7 | 8 | 'use strict'; 9 | 10 | // We should use a reference directly the original application-stub object in case Box.Application gets stubbed out 11 | var application = Box.Application; 12 | 13 | // function stubs that are automatically included on a TestServiceProvider 14 | var APPLICATION_CONTEXT_STUBS = [ 15 | // Shared between Application and Context 16 | 'broadcast', 'getGlobalConfig', 'reportError', 'reportWarning', 'reportInfo', 17 | 18 | // Application (only ones that should be called from a service) 19 | 'start', 'stop', 'startAll', 'stopAll', 'isStarted', 20 | 21 | // Context (module/behavior only) - getElement done separately 22 | 'getConfig' 23 | ]; 24 | 25 | /** 26 | * Return a function stub that will throw an error if the test code does not properly mock out dependencies. 27 | * @param {string} method The name of the method being invoked 28 | * @returns {Function} A function stub 29 | */ 30 | function functionStub(method) { 31 | /* eslint-disable no-extra-parens */ 32 | return (function(methodKey) { 33 | return function() { 34 | throw new Error('Unexpected call to method "' + methodKey + '". You must stub this method out.'); 35 | }; 36 | }(method)); 37 | /* eslint-enable no-extra-parens */ 38 | } 39 | 40 | /** 41 | * Check if service is allowed. This should be replaced by Array.prototype.indexOf when we drop IE 8 support 42 | * @param {string} serviceName The name of the service being checked 43 | * @param {string[]} allowedServicesList The list of services allowed by the user 44 | * @returns {boolean} Returns true if service is in the allowed list 45 | * @private 46 | */ 47 | function isServiceAllowed(serviceName, allowedServicesList) { 48 | for (var i = 0, len = allowedServicesList.length; i < len; i++) { 49 | if (allowedServicesList[i] === serviceName) { 50 | return true; 51 | } 52 | } 53 | return false; 54 | } 55 | 56 | /** 57 | * This object is used as a stub for application/context that is normally passed into services/modules/behaviors at create time. 58 | * It exposes the stubbed services passed in through the getService() method. Also allows the use of pre-registered services. 59 | * @param {Object} serviceStubs A map of service stubs 60 | * @param {string[]} [allowedServicesList] List of real services to allow access to 61 | * @constructor 62 | */ 63 | Box.TestServiceProvider = function(serviceStubs, allowedServicesList) { 64 | this.stubs = serviceStubs || {}; 65 | this.allowedServicesList = allowedServicesList || []; 66 | this.serviceInstances = {}; // Stores the instances of pre-registered services 67 | }; 68 | 69 | Box.TestServiceProvider.prototype = { 70 | constructor: Box.TestServiceProvider, 71 | 72 | /** 73 | * Will retrieve either a service stub (prioritized) or the real service. Returns null if neither exists. 74 | * @param {string} serviceName The name of the service being retrieved 75 | * @returns {?Object} A service object or null if none exists 76 | * @throws {Error} Will throw an error if service does not exist 77 | */ 78 | getService: function(serviceName) { 79 | var service = this.stubs[serviceName]; 80 | 81 | // Return a service stub if found 82 | if (service) { 83 | return service; 84 | } 85 | 86 | // Check if this service is allowed to be pre-registered 87 | if (isServiceAllowed(serviceName, this.allowedServicesList)) { 88 | // Now check if we've already created the service instance 89 | if (this.serviceInstances.hasOwnProperty(serviceName)) { 90 | return this.serviceInstances[serviceName]; 91 | } else { 92 | var preRegisteredService = application.getServiceForTest(serviceName, this); 93 | if (preRegisteredService) { 94 | // Save the instance for the next call to getService() 95 | this.serviceInstances[serviceName] = preRegisteredService; 96 | return preRegisteredService; 97 | } else { 98 | throw new Error('Service "' + serviceName + '" does not exist.'); 99 | } 100 | } 101 | 102 | } else { 103 | throw new Error('Service "' + serviceName + '" is not on the `allowedServiceList`. Use "new Box.TestServiceProvider({ ...stubs... }, [\'' + serviceName + '\']);" or stub the service out.'); 104 | } 105 | }, 106 | 107 | /** 108 | * Checks if a service exists 109 | * @param {string} serviceName The name of the service to check. 110 | * @returns {boolean} True, if service exist. False, otherwise. 111 | */ 112 | hasService: function(serviceName) { 113 | return this.stubs.hasOwnProperty(serviceName) || isServiceAllowed(serviceName, this.allowedServicesList) && application.hasService(serviceName); 114 | }, 115 | 116 | /** 117 | * Retrieves a global var (this is the actual implementation for convenience in testing) 118 | * @param {string} name The name of the global 119 | * @returns {?*} The global object referenced or null if it does not exist 120 | */ 121 | getGlobal: function(name) { 122 | if (name in window) { 123 | return window[name]; 124 | } else { 125 | return null; 126 | } 127 | } 128 | }; 129 | 130 | // Add stubbed functions onto prototype for testing convenience 131 | (function() { 132 | var stubName; 133 | for (var i = 0, len = APPLICATION_CONTEXT_STUBS.length; i < len; i++) { 134 | stubName = APPLICATION_CONTEXT_STUBS[i]; 135 | Box.TestServiceProvider.prototype[stubName] = functionStub(stubName); 136 | } 137 | }()); 138 | 139 | }()); 140 | -------------------------------------------------------------------------------- /lib/wrap-end.partial: -------------------------------------------------------------------------------- 1 | if (typeof define === 'function' && define.amd) { 2 | // AMD 3 | define('t3', [], function() { 4 | return Box; 5 | }); 6 | } else if (typeof module === 'object' && typeof module.exports === 'object') { 7 | // CommonJS/npm, we want to export Box instead of assigning to global Window 8 | module.exports = Box; 9 | } else { 10 | // Make sure not to override Box namespace 11 | window.Box = window.Box || {}; 12 | 13 | // Copy all properties onto namespace (ES3 safe for loop) 14 | for (var key in Box) { 15 | if (Box.hasOwnProperty(key)) { 16 | window.Box[key] = Box[key]; 17 | } 18 | } 19 | } 20 | 21 | // Potentially window is not defined yet, so bind to 'this' instead 22 | }(typeof window !== 'undefined' ? window : this)); 23 | // End Wrapper 24 | -------------------------------------------------------------------------------- /lib/wrap-start.partial: -------------------------------------------------------------------------------- 1 | // Start wrapper 2 | // We use this to make sure we don't assign globals unless we actually want to 3 | (function(window) { 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3js", 3 | "description": "T3 Javascript Framework", 4 | "version": "2.7.0", 5 | "author": "Box (https://www.box.com/)", 6 | "files": [ 7 | "LICENSE", 8 | "README.md", 9 | "dist" 10 | ], 11 | "bugs": "http://github.com/box/t3js/issues", 12 | "homepage": "http://t3js.org", 13 | "license": "Apache-2.0", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/box/t3js" 17 | }, 18 | "scripts": { 19 | "test": "node Makefile.js test", 20 | "test-watch": "node Makefile.js test-watch", 21 | "lint": "node Makefile.js lint", 22 | "dist": "node Makefile.js dist", 23 | "patch": "node Makefile.js patch", 24 | "minor": "node Makefile.js minor", 25 | "major": "node Makefile.js major" 26 | }, 27 | "main": "dist/t3-native.js", 28 | "devDependencies": { 29 | "assertive-chai": "^1.0.2", 30 | "dateformat": "^1.0.11", 31 | "eslint": "^1.1.0", 32 | "jquery": "^1.11.1", 33 | "karma": "^0.12.31", 34 | "karma-coverage": "^0.2.7", 35 | "karma-mocha": "^0.1.10", 36 | "karma-mocha-reporter": "^1.0.2", 37 | "karma-phantomjs-launcher": "^0.1.4", 38 | "karma-threshold-reporter": "^0.1.15", 39 | "leche": "^2.1.1", 40 | "mocha": "^2.2.1", 41 | "phantomjs": "^1.9.16", 42 | "semver": "^4.3.3", 43 | "shelljs": "^0.4.0", 44 | "shelljs-nodecli": "^0.1.1", 45 | "sinon": "^1.14.1", 46 | "uglify-js": "^2.4.17" 47 | }, 48 | "keywords": [ 49 | "javascript", 50 | "framework", 51 | "browser", 52 | "client-side", 53 | "modular", 54 | "component" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "window": false, 7 | "document": false, 8 | "Box": false, 9 | "$": false, 10 | "mocha": false, 11 | "chai": false, 12 | "sinon": false, 13 | "assert": true, 14 | "leche": true 15 | }, 16 | "rules": { 17 | "brace-style": 2, 18 | "func-style": [2, "declaration"], 19 | "strict": [2, "function"], 20 | "guard-for-in": 2, 21 | "no-floating-decimal": 2, 22 | "no-underscore-dangle": 0, 23 | "no-nested-ternary": 2, 24 | "quotes": [2, "single"], 25 | "radix": 2, 26 | "wrap-iife": 2, 27 | "space-after-keywords": 2, 28 | "valid-jsdoc": [2, { 29 | "prefer": { 30 | "return": "returns" 31 | } 32 | }], 33 | // We allow unused args 34 | "no-unused-vars": [2, {"args": "none"}] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/api-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for T3 API (package.json and dist files) 3 | * @author Box 4 | */ 5 | 6 | /* eslint-env node */ 7 | /* eslint strict: [2, "global"] */ 8 | 9 | 'use strict'; 10 | 11 | // @NOTE(nzakas): This file runs in Node.js, not Karma! 12 | 13 | //------------------------------------------------------------------------------ 14 | // Requirements 15 | //------------------------------------------------------------------------------ 16 | 17 | var assert = require('assertive-chai').assert, 18 | leche = require('leche'), 19 | path = require('path'), 20 | defaultT3 = require('../dist/t3'), 21 | nativeT3 = require('../dist/t3-native'), 22 | jqueryT3 = require('../dist/t3-jquery'), 23 | pkg = require('../package.json'); 24 | 25 | //------------------------------------------------------------------------------ 26 | // Tests 27 | //------------------------------------------------------------------------------ 28 | 29 | describe('API', function() { 30 | 31 | describe('Defaults', function() { 32 | it('should use native DOM in default file', function() { 33 | assert.equal(defaultT3.DOM.type, 'native'); 34 | }); 35 | 36 | it('should use native DOM in native file', function() { 37 | assert.equal(nativeT3.DOM.type, 'native'); 38 | }); 39 | 40 | it('should use jQuery DOM in jQuery file', function() { 41 | assert.equal(jqueryT3.DOM.type, 'jquery'); 42 | }); 43 | }); 44 | 45 | describe('Exports', function() { 46 | 47 | leche.withData({ 48 | 'Default T3': defaultT3, 49 | 'Native T3': nativeT3, 50 | 'jQuery T3': jqueryT3 51 | }, function(T3) { 52 | 53 | leche.withData([ 54 | 'DOM', 55 | 'DOMEventDelegate', 56 | 'EventTarget', 57 | 'Application', 58 | 'Context' 59 | ], function(name) { 60 | it('should have Box.' + name, function() { 61 | assert.isDefined(T3[name]); 62 | }); 63 | }); 64 | 65 | }); 66 | 67 | }); 68 | 69 | describe('package.json', function() { 70 | 71 | it('should export native T3', function() { 72 | assert.equal(require(path.join('../', pkg.main)), nativeT3); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /tests/context-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for module context 3 | * @author Box 4 | */ 5 | 6 | describe('Box.Context', function() { 7 | 8 | 'use strict'; 9 | 10 | var sandbox = sinon.sandbox.create(); 11 | var element = document.body; 12 | 13 | afterEach(function() { 14 | sandbox.verifyAndRestore(); 15 | }); 16 | 17 | describe('new Box.Context', function(){ 18 | it('should have Box.Context as its constructor', function(){ 19 | var context = new Box.Context(); 20 | 21 | assert.strictEqual(context.constructor, Box.Context); 22 | }); 23 | }); 24 | 25 | describe('broadcast()', function() { 26 | 27 | it('should pass through to application when called', function() { 28 | var context, 29 | message = 'foo', 30 | application = { 31 | broadcast: function() {} 32 | }; 33 | 34 | sandbox.mock(application).expects('broadcast').withArgs(message); 35 | context = new Box.Context(application, element); 36 | context.broadcast(message); 37 | }); 38 | 39 | it('should pass through to application when called with arguments', function() { 40 | var context, 41 | message = 'foo', 42 | data = {}, 43 | application = { 44 | broadcast: function() {} 45 | }; 46 | 47 | sandbox.mock(application).expects('broadcast').withArgs(message, data); 48 | context = new Box.Context(application, element); 49 | context.broadcast(message, data); 50 | }); 51 | 52 | }); 53 | 54 | describe('getService()', function() { 55 | 56 | it('should pass through to application when called with service name', function() { 57 | var context, 58 | serviceName = 'foo', 59 | service = {}, 60 | application = { 61 | getService: function() {} 62 | }; 63 | 64 | sandbox.mock(application).expects('getService').withArgs(serviceName).returns(service); 65 | context = new Box.Context(application, element); 66 | assert.equal(context.getService(serviceName), service, 'getService() should return correct service'); 67 | }); 68 | 69 | }); 70 | 71 | describe('hasService()', function() { 72 | 73 | it('should return true when Application has the service', function() { 74 | var context, 75 | serviceName = 'foo', 76 | application = { 77 | hasService: function() {} 78 | }; 79 | 80 | sandbox.mock(application).expects('hasService').withArgs(serviceName).returns(true); 81 | context = new Box.Context(application, element); 82 | assert.isTrue(context.hasService(serviceName), 'hasService() should return true'); 83 | }); 84 | 85 | it('should return false when Application has the service', function() { 86 | var context, 87 | serviceName = 'foo', 88 | application = { 89 | hasService: function() {} 90 | }; 91 | 92 | sandbox.mock(application).expects('hasService').withArgs(serviceName).returns(false); 93 | context = new Box.Context(application, element); 94 | assert.isFalse(context.hasService(serviceName), 'hasService() should return false'); 95 | }); 96 | 97 | }); 98 | 99 | describe('getConfig()', function() { 100 | 101 | it('should pass through module element and config name to application.getModuleConfig() when called', function() { 102 | var context, 103 | config = {}, 104 | application = { 105 | getModuleConfig: function() {} 106 | }; 107 | 108 | sandbox.mock(application).expects('getModuleConfig').withArgs(element, 'foo').returns(config); 109 | context = new Box.Context(application, element); 110 | context.getConfig('foo'); 111 | }); 112 | }); 113 | 114 | describe('getGlobal()', function() { 115 | 116 | it('should return the window-scope var when it exists', function () { 117 | var application = { 118 | getGlobal: function () {} 119 | }; 120 | 121 | sandbox.stub(application, 'getGlobal').withArgs('foo').returns('bar'); 122 | 123 | var context = new Box.Context(application, element); 124 | assert.strictEqual(context.getGlobal('foo'), 'bar', 'global var returned'); 125 | }); 126 | 127 | }); 128 | 129 | 130 | describe('getGlobalConfig()', function() { 131 | 132 | it('should pass through to application when called', function() { 133 | var context, 134 | application = { 135 | getGlobalConfig: function() {} 136 | }; 137 | 138 | sandbox.mock(application).expects('getGlobalConfig').withArgs('foo').returns('bar'); 139 | context = new Box.Context(application, element); 140 | assert.equal(context.getGlobalConfig('foo'), 'bar', 'correct config value returned'); 141 | }); 142 | 143 | }); 144 | 145 | describe('reportError()', function() { 146 | 147 | it('should pass through to application when called', function() { 148 | var context, 149 | application = { 150 | reportError: function() {} 151 | }, 152 | exception = new Error('test error'); 153 | 154 | sandbox.mock(application).expects('reportError').withArgs(exception); 155 | context = new Box.Context(application, element); 156 | 157 | context.reportError(exception); 158 | }); 159 | 160 | }); 161 | 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /tests/dom-event-delegate-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for DOMEventDelegate 3 | * @author Box 4 | */ 5 | 6 | 7 | describe('Box.DOMEventDelegate', function() { 8 | 9 | 'use strict'; 10 | 11 | leche.withData({ 12 | native: [Box.NativeDOM], 13 | jquery: [Box.JQueryDOM] 14 | }, function(dom) { 15 | 16 | var sandbox = sinon.sandbox.create(), 17 | delegate; 18 | 19 | before(function() { 20 | Box.DOM = dom; 21 | var fixture = document.createElement('div'); 22 | fixture.id = 'mocha-fixture'; 23 | document.body.appendChild(fixture); 24 | }); 25 | 26 | afterEach(function () { 27 | sandbox.verifyAndRestore(); 28 | 29 | delegate = null; 30 | $('#mocha-fixture').empty(); 31 | }); 32 | 33 | after(function() { 34 | $('#mocha-fixture').remove(); 35 | }); 36 | 37 | /** 38 | * Use jQuery to click an element. Need to do this because PhantomJS 39 | * doesn't properly handle clicks otherwise. 40 | * @param {HTMLElement} element The element to click. 41 | * @returns {void} 42 | * @private 43 | */ 44 | function click(element) { 45 | $(element).click(); 46 | } 47 | 48 | describe('attachEvents()', function() { 49 | 50 | var testElement; 51 | 52 | beforeEach(function() { 53 | testElement = $('
')[0]; 54 | $('#mocha-fixture').append(testElement); 55 | }); 56 | 57 | it('should respond to click when called with a click handler', function() { 58 | 59 | delegate = new Box.DOMEventDelegate(testElement, { 60 | onclick: sandbox.mock() 61 | }); 62 | 63 | delegate.attachEvents(); 64 | 65 | click(testElement.firstChild); 66 | }); 67 | 68 | it('should pass three arguments to onclick when a data-type element is clicked', function() { 69 | 70 | delegate = new Box.DOMEventDelegate(testElement, { 71 | onclick: sandbox.mock().withExactArgs( 72 | sinon.match({ type: 'click' }), 73 | testElement.firstChild, 74 | 'submit' 75 | ) 76 | }); 77 | 78 | delegate.attachEvents(); 79 | 80 | click(testElement.firstChild); 81 | }); 82 | 83 | it('should pass three arguments to onclick when element with both data-type and data-module is clicked', function() { 84 | 85 | delegate = new Box.DOMEventDelegate(testElement, { 86 | onclick: sandbox.mock().withExactArgs( 87 | sinon.match({ type: 'click' }), 88 | testElement, 89 | 'form-type' 90 | ) 91 | }); 92 | 93 | delegate.attachEvents(); 94 | 95 | testElement.setAttribute('data-type', 'form-type'); 96 | click(document.querySelector('#non-typed-button')); 97 | }); 98 | 99 | it('should pass three arguments to onclick when a non-data-type element is clicked', function() { 100 | 101 | delegate = new Box.DOMEventDelegate(testElement, { 102 | onclick: sandbox.mock().withExactArgs( 103 | sinon.match({ type: 'click' }), 104 | null, 105 | '' 106 | ) 107 | }); 108 | 109 | delegate.attachEvents(); 110 | 111 | testElement.firstChild.removeAttribute('data-type'); 112 | click(testElement.firstChild); 113 | }); 114 | 115 | it('should pass three arguments to onclick when element has no ancestor with a data-type or data-module', function() { 116 | 117 | delegate = new Box.DOMEventDelegate(testElement, { 118 | onclick: sandbox.mock().withExactArgs( 119 | sinon.match({ type: 'click' }), 120 | null, 121 | '' 122 | ) 123 | }); 124 | 125 | delegate.attachEvents(); 126 | 127 | testElement.removeAttribute('data-module'); 128 | testElement.firstChild.removeAttribute('data-type'); 129 | click(testElement.firstChild); 130 | }); 131 | 132 | it('should respond to click only once when called twice', function() { 133 | 134 | delegate = new Box.DOMEventDelegate(testElement, { 135 | onclick: sandbox.mock() // enforces one call 136 | }); 137 | 138 | delegate.attachEvents(); 139 | delegate.attachEvents(); 140 | 141 | click(testElement.firstChild); 142 | }); 143 | 144 | it('should respond to custom events when a custom event type list is specified', function() { 145 | 146 | delegate = new Box.DOMEventDelegate(testElement, { 147 | ontouchstart: sandbox.mock() 148 | }, ['touchstart']); 149 | 150 | delegate.attachEvents(); 151 | 152 | var event = document.createEvent('Event'); 153 | event.initEvent('touchstart', true, true); 154 | 155 | testElement.firstChild.dispatchEvent(event); 156 | }); 157 | 158 | it('should respond only to custom events when a custom event type list is specified', function() { 159 | 160 | delegate = new Box.DOMEventDelegate(testElement, { 161 | onclick: sandbox.mock().never() 162 | }, ['onsubmit']); 163 | 164 | delegate.attachEvents(); 165 | 166 | click(testElement.firstChild); 167 | }); 168 | 169 | 170 | }); 171 | 172 | describe('detachEvents()', function() { 173 | 174 | it('should not respond to click when called with a click handler', function() { 175 | var testElement = $('
')[0]; 176 | $('#mocha-fixture').append(testElement); 177 | 178 | delegate = new Box.DOMEventDelegate(testElement, { 179 | onclick: sandbox.mock().never() 180 | }); 181 | 182 | delegate.attachEvents(); 183 | delegate.detachEvents(); 184 | click(testElement.firstChild); 185 | }); 186 | 187 | }); 188 | 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /tests/dom-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for DOM abstraction layer 3 | * @author Box 4 | */ 5 | 6 | 7 | describe('Box.DOM', function() { 8 | 'use strict'; 9 | 10 | leche.withData({ 11 | native: [Box.NativeDOM], 12 | jquery: [Box.JQueryDOM] 13 | }, function(dom) { 14 | var sandbox = sinon.sandbox.create(); 15 | 16 | var testModule, 17 | nestedModule; 18 | 19 | before(function() { 20 | Box.DOM = dom; 21 | var fixture = document.createElement('div'); 22 | fixture.id = 'mocha-fixture'; 23 | document.body.appendChild(fixture); 24 | }); 25 | 26 | beforeEach(function() { 27 | testModule = $('
')[0]; 28 | nestedModule = $('
')[0]; 29 | $('#mocha-fixture').append(testModule, nestedModule); 30 | }); 31 | 32 | afterEach(function() { 33 | sandbox.verifyAndRestore(); 34 | 35 | $('#mocha-fixture').empty(); 36 | }); 37 | 38 | after(function() { 39 | $('#mocha-fixture').remove(); 40 | }); 41 | 42 | describe('type', function() { 43 | it('should match type value of passed in event type', function() { 44 | assert.equal(Box.DOM.type, dom.type); 45 | }); 46 | }); 47 | 48 | describe('query()', function() { 49 | it('should return first element when multiples exist', function() { 50 | var testEl = Box.DOM.query(document.getElementById('mocha-fixture'), 'div'); 51 | 52 | assert.equal(testEl, testModule); 53 | }); 54 | 55 | it('should return null when no matches exist', function() { 56 | var testEl = Box.DOM.query(document.getElementById('mocha-fixture'), 'article'); 57 | 58 | assert.isNull(testEl); 59 | }); 60 | 61 | it('should return the element when exactly one match exists', function() { 62 | var testEl = Box.DOM.query(document.getElementById('mocha-fixture'), 'button'); 63 | var testBtn = document.getElementById('module-target'); 64 | 65 | assert.equal(testEl, testBtn); 66 | }); 67 | }); 68 | 69 | describe('queryAll()', function() { 70 | it('should return all elements when multiples exist', function() { 71 | var testEl = Box.DOM.queryAll(document.getElementById('mocha-fixture'), 'div'); 72 | 73 | assert.equal(testEl.length, 3); 74 | }); 75 | 76 | it('should return an empty array when no matches exist', function() { 77 | var testEl = Box.DOM.queryAll(document.getElementById('mocha-fixture'), 'article'); 78 | 79 | assert.equal(testEl.length, 0); 80 | }); 81 | 82 | it('should return an array of one element when exactly one match exists', function() { 83 | var testEl = Box.DOM.queryAll(document.getElementById('mocha-fixture'), 'button'); 84 | var testBtn = document.getElementById('module-target'); 85 | 86 | assert.equal(testEl.length, 1); 87 | assert.equal(testEl[0].id, testBtn.id); 88 | }); 89 | }); 90 | 91 | describe('on()', function() { 92 | it('should call an attached function', function() { 93 | var testBtn = document.getElementById('module-target'); 94 | Box.DOM.on(testBtn, 'click', sandbox.mock()); 95 | 96 | testBtn.click(); 97 | }); 98 | }); 99 | 100 | describe('off()', function() { 101 | it('should not call a function when it\'s listener has been turned off', function() { 102 | var neverEver = sandbox.mock().never(); 103 | var testBtn = document.getElementById('module-target'); 104 | Box.DOM.on(testBtn, 'click', neverEver); 105 | Box.DOM.off(testBtn, 'click', neverEver); 106 | 107 | testBtn.click(); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/event-target-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for event-target 3 | * @author Box 4 | */ 5 | 6 | describe('Box.EventTarget', function() { 7 | 8 | 'use strict'; 9 | 10 | var sandbox = sinon.sandbox.create(); 11 | var eventTarget; 12 | 13 | beforeEach(function() { 14 | eventTarget = new Box.EventTarget(); 15 | }); 16 | 17 | afterEach(function() { 18 | sandbox.verifyAndRestore(); 19 | }); 20 | 21 | describe('Multiple event handlers', function() { 22 | 23 | it('should be called for custom event', function() { 24 | eventTarget.on('myevent', sandbox.mock()); 25 | eventTarget.on('myevent', sandbox.mock()); 26 | eventTarget.fire('myevent'); 27 | }); 28 | 29 | it('should be prevented for duplicate handlers', function() { 30 | var callback = sandbox.mock(); 31 | eventTarget.on('myevent', callback); 32 | eventTarget.on('myevent', callback); 33 | eventTarget.on('myevent', callback); 34 | eventTarget.fire('myevent'); 35 | }); 36 | 37 | }); 38 | 39 | describe('Separate event handlers', function() { 40 | 41 | it('should be called for separate custom events', function() { 42 | eventTarget.on('myevent1', sandbox.mock()); 43 | eventTarget.on('myevent2', sandbox.mock().never()); 44 | eventTarget.fire('myevent1'); 45 | }); 46 | 47 | }); 48 | 49 | describe('Event handler', function() { 50 | 51 | it('should be called for custom event', function() { 52 | eventTarget.on('myevent', sandbox.mock()); 53 | eventTarget.fire('myevent'); 54 | }); 55 | 56 | it('should be called for custom event', function() { 57 | eventTarget.on('myevent', sandbox.mock()); 58 | eventTarget.fire('myevent'); 59 | }); 60 | 61 | it('should be called with custom event object for custom event', function() { 62 | var handler = sandbox.mock().withArgs({ 63 | type: 'myevent', 64 | data: undefined 65 | }); 66 | 67 | eventTarget.on('myevent', handler); 68 | eventTarget.fire('myevent'); 69 | }); 70 | 71 | it('should be called with custom event object and extra data for custom event', function() { 72 | var handler = sandbox.mock().withArgs({ 73 | type: 'myevent', 74 | data: { 75 | foo: 'bar', 76 | time: 'now' 77 | } 78 | }); 79 | 80 | eventTarget.on('myevent', handler); 81 | eventTarget.fire('myevent', { 82 | foo: 'bar', 83 | time: 'now' 84 | }); 85 | }); 86 | 87 | it('should not be called for custom event after being removed', function() { 88 | var handler = sandbox.spy(); 89 | eventTarget.on('myevent', handler); 90 | 91 | eventTarget.off('myevent', handler); 92 | 93 | eventTarget.fire('myevent'); 94 | assert.ok(handler.notCalled); 95 | }); 96 | 97 | it('should be called even after another event handler for the same type removes itself', function() { 98 | var handler1, 99 | handler2 = sandbox.spy(); 100 | 101 | handler1 = function() { 102 | // this handler removes itself 103 | this.off('myevent', handler1); 104 | }; 105 | 106 | eventTarget.on('myevent', handler1); 107 | eventTarget.on('myevent', handler2); 108 | 109 | eventTarget.fire('myevent'); 110 | assert.ok(handler2.called); 111 | }); 112 | 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/test-service-provider-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for TestServiceProvider 3 | * @author Box 4 | */ 5 | 6 | describe('Box.TestServiceProvider', function() { 7 | 8 | 'use strict'; 9 | 10 | var sandbox = sinon.sandbox.create(); 11 | var testServiceProvider; 12 | 13 | beforeEach(function() { 14 | Box.Application.reset(); 15 | }); 16 | 17 | afterEach(function() { 18 | sandbox.verifyAndRestore(); 19 | 20 | Box.Application.reset(); 21 | }); 22 | 23 | describe('new Box.TestServiceProvider', function() { 24 | 25 | it('should have Box.TestServiceProvider as its constructor', function(){ 26 | testServiceProvider = new Box.TestServiceProvider(); 27 | 28 | assert.strictEqual(testServiceProvider.constructor, Box.TestServiceProvider); 29 | }); 30 | 31 | it('should set this.stubs to serviceStubs when called', function() { 32 | var serviceStub = { 33 | bar: 'baz' 34 | }; 35 | testServiceProvider = new Box.TestServiceProvider({ 36 | foo: serviceStub 37 | }); 38 | 39 | assert.propertyVal(testServiceProvider.stubs, 'foo', serviceStub); 40 | }); 41 | 42 | it('should set allowedServicesList to this.allowedServicesList when called', function() { 43 | testServiceProvider = new Box.TestServiceProvider({}, ['example-service']); 44 | 45 | assert.deepEqual(testServiceProvider.allowedServicesList, ['example-service']); 46 | assert.deepEqual(testServiceProvider.serviceInstances, {}); 47 | }); 48 | 49 | }); 50 | 51 | describe('getService()', function() { 52 | 53 | var stubbedService1 = { service: '1'}, 54 | stubbedService2 = { service: '2'}, 55 | preRegisteredService2 = { service: 'pre-2'}, 56 | preRegisteredService3 = { service: 'pre-3' }; 57 | 58 | beforeEach(function() { 59 | // Pre-register services 60 | var getServiceForTestStub = sandbox.stub(Box.Application, 'getServiceForTest'); 61 | getServiceForTestStub.withArgs('service2').returns(preRegisteredService2); 62 | getServiceForTestStub.withArgs('service3').returns(preRegisteredService3); 63 | getServiceForTestStub.returns(null); 64 | 65 | testServiceProvider = new Box.TestServiceProvider({ 66 | service1: stubbedService1, 67 | service2: stubbedService2 68 | }, ['service2', 'service3', 'service4']); 69 | }); 70 | 71 | it('should return the stubbed version of a service when a stub is available', function() { 72 | assert.deepEqual(testServiceProvider.getService('service1'), stubbedService1); 73 | }); 74 | 75 | it('should return the stubbed version of a service when a stub and a pre-registered service are available', function() { 76 | assert.deepEqual(testServiceProvider.getService('service2'), stubbedService2); 77 | }); 78 | 79 | it('should return the pre-registered version of a service when a pre-registered service is available', function() { 80 | assert.deepEqual(testServiceProvider.getService('service3'), preRegisteredService3); 81 | }); 82 | 83 | it('should return the same pre-registered service instance when called multiple times', function() { 84 | assert.deepEqual(testServiceProvider.getService('service3'), preRegisteredService3); 85 | assert.strictEqual(testServiceProvider.getService('service3'), testServiceProvider.getService('service3')); 86 | }); 87 | 88 | it('should throw an error when service is not available and service is on the allowedServiceList', function() { 89 | assert.throws(function() { 90 | testServiceProvider.getService('service4'); 91 | }, 'Service "not-available-service" does not exist.'); 92 | }); 93 | 94 | it('should throw an error when service is not available and service is not on the allowedServiceList', function() { 95 | assert.throws(function() { 96 | testServiceProvider.getService('not-available-service'); 97 | }, 'Service "not-available-service" is not on the `allowedServiceList`. Use "new Box.TestServiceProvider({ ...stubs... }, [\'not-available-service\']);" or stub the service out.'); 98 | }); 99 | 100 | }); 101 | 102 | describe('hasService()', function() { 103 | 104 | beforeEach(function() { 105 | // Setup Application 106 | var hasServiceStub = sandbox.stub(Box.Application, 'hasService'); 107 | hasServiceStub.withArgs('service2').returns(true); 108 | hasServiceStub.withArgs('service4').returns(true); 109 | hasServiceStub.returns(false); 110 | 111 | testServiceProvider = new Box.TestServiceProvider({ 112 | service1: {} 113 | }, ['service2', 'service3']); 114 | }); 115 | 116 | it('should return false when service does not exist', function() { 117 | assert.isFalse(testServiceProvider.hasService('non-existent-service')); 118 | }); 119 | 120 | it('should return true when service is stubbed', function() { 121 | assert.isTrue(testServiceProvider.hasService('service1')); 122 | }); 123 | 124 | it('should return true when service is allowed and available', function() { 125 | assert.isTrue(testServiceProvider.hasService('service2')); 126 | }); 127 | 128 | it('should return false when service is allowed but not available', function() { 129 | assert.isFalse(testServiceProvider.hasService('service3')); 130 | }); 131 | 132 | it('should return false when service is available but not allowed', function() { 133 | assert.isFalse(testServiceProvider.hasService('service4')); 134 | }); 135 | 136 | }); 137 | 138 | describe('getGlobal()', function() { 139 | 140 | it('should return window var when requesting an existing property', function() { 141 | window.foo = 'bar'; 142 | assert.strictEqual(testServiceProvider.getGlobal('foo'), window.foo); 143 | delete window.foo; 144 | }); 145 | 146 | it('should return null when requesting a non-existent property', function() { 147 | assert.isNull(testServiceProvider.getGlobal('foobar')); 148 | }); 149 | 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /tests/utils/common-setup.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: 0, no-unused-vars: 0 */ 2 | 3 | var assert = chai.assert; 4 | --------------------------------------------------------------------------------