├── .gitignore
├── CHANGELOG.md
├── Gruntfile.coffee
├── LICENSE
├── README.md
├── bower.json
├── e2e
├── fcsaNumber.e2e.coffee
├── fcsaNumber.e2e.js
├── protractor.config
└── web
│ ├── app.js
│ └── public
│ ├── angular.js
│ ├── fcsaNumber.js
│ └── index.html
├── karma.conf.js
├── package.json
├── src
├── fcsaNumber.coffee
├── fcsaNumber.js
└── fcsaNumber.min.js
└── test
├── fcsaNumber.spec.coffee
├── fcsaNumber.spec.js
├── fcsaNumberConfig.spec.coffee
└── fcsaNumberConfig.spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 1.5.3
4 |
5 | * Bug Fixes
6 | * Fixed a bug where `-1.2` was being formatted as `-1-1.2`
7 |
8 | ### 1.5.2
9 |
10 | * Bug Fixes
11 | * `preventInvalidInput` is correctly working again
12 |
13 | ### 1.5.1
14 |
15 | * Bug Fixes
16 | * Fixed issue where overriding a global option on a specific input element would override that
17 | option for all input elements
18 |
19 | ### 1.5.0
20 |
21 | * Enhancements
22 | * Can set default options with `fcsaNumberConfigProvider.setDefaultOptions()`
23 |
24 | ### 1.4.1
25 |
26 | * Bug Fixes
27 | * `prepend` and `append` values are removed on focus
28 |
29 | ### 1.4
30 |
31 | * Enhancements
32 | * Added `prepend` option
33 | * Added `append` option
34 |
35 | ### 1.3
36 |
37 | * Enhancements
38 | * Not adding commas to the decimal portion of the number
39 | * Allowing commas to be entered by the user. Thanks @jbulat
40 | * A hyphen only value is marked invalid
41 |
42 | ### 1.2.1
43 |
44 | * Refactorings
45 | * using `angular.element` instead of `$` and therefore no longer requiring jQuery to be used
46 |
47 | ### 1.2
48 |
49 | * Enhancements
50 | * added 'preventInvalidInput' option
51 |
52 | ### 1.1
53 |
54 | * Enhancements
55 | * renamed `decimals` option to `maxDecimals`
56 | * renamed `digits` option to `maxDigits`
57 |
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (grunt) ->
2 |
3 | grunt.initConfig
4 | coffee:
5 | options:
6 | bare: false
7 | compile:
8 | files:
9 | 'src/fcsaNumber.js': 'src/fcsaNumber.coffee'
10 | 'test/fcsaNumber.spec.js': 'test/fcsaNumber.spec.coffee'
11 | 'test/fcsaNumberConfig.spec.js': 'test/fcsaNumberConfig.spec.coffee'
12 | 'e2e/fcsaNumber.e2e.js': 'e2e/fcsaNumber.e2e.coffee'
13 | pkg: grunt.file.readJSON('package.json')
14 | uglify:
15 | options:
16 | banner: '/*! <%= pkg.name %> (version <%= pkg.version %>) <%= grunt.template.today("yyyy-mm-dd") %> */\n'
17 | build:
18 | src: 'src/fcsaNumber.js'
19 | dest: 'src/fcsaNumber.min.js'
20 | file_append:
21 | default_options:
22 | files:
23 | 'src/fcsaNumber.js':
24 | prepend: '/*! <%= pkg.name %> (version <%= pkg.version %>) <%= grunt.template.today("yyyy-mm-dd") %> */\n'
25 | copy:
26 | web_angular:
27 | src: 'bower_components/angular/angular.js'
28 | dest: 'e2e/web/public/angular.js'
29 | web_fcsaNumber:
30 | src: 'src/fcsaNumber.js'
31 | dest: 'e2e/web/public/fcsaNumber.js'
32 | express:
33 | dev:
34 | options:
35 | script: 'e2e/web/app.js'
36 | nospawn: true
37 | delay: 5
38 | shell:
39 | protractor:
40 | options:
41 | stdout: true
42 | command: 'protractor e2e/protractor.config'
43 | watch:
44 | files: [
45 | 'src/fcsaNumber.coffee'
46 | 'test/fcsaNumber.spec.coffee'
47 | 'test/fcsaNumberConfig.spec.coffee'
48 | 'e2e/fcsaNumber.e2e.coffee'
49 | ]
50 | tasks: 'default'
51 | karma:
52 | files: [
53 | 'src/fcsaNumber.js'
54 | 'test/fcsaNumber.spec.js'
55 | 'test/fcsaNumberConfig.spec.js'
56 | ]
57 | tasks: ['karma:unit:run']
58 | karma:
59 | unit:
60 | configFile: 'karma.conf.js'
61 | background: true
62 | continuous:
63 | configFile: 'karma.conf.js'
64 | singleRun: true
65 | browsers: ['PhantomJS']
66 |
67 |
68 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks)
69 |
70 | grunt.registerTask 'default', ['coffee', 'uglify', 'file_append', 'copy:web_angular', 'copy:web_fcsaNumber']
71 | grunt.registerTask 'e2e', ['default', 'express', 'shell:protractor']
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FCSA Number
2 |
3 | An Angular directive that validates numbers and adds commas for the thousands separator.
4 |
5 | So when the user enters `1000` into a textbox and tabs out, the value will be formatted to include the thousands separator and display: `1,000`
6 |
7 | If an invalid number is entered, the textbox will be invalid with the `fcsaNumber` error.
8 |
9 | ## Installation
10 |
11 | #### With Bower
12 |
13 | bower install angular-fcsa-number
14 |
15 | Then reference `angular-fcsa-number/src/fcsaNumber.js` in your project.
16 |
17 | #### Manually
18 |
19 | Copy `src/fcsaNumber.js` into your project and reference it.
20 |
21 | ## Quick Start
22 |
23 | Add the `fcsa-number` module as a dependency to you Angular app.
24 |
25 | angular.module('yourApp', ['fcsa-number']);
26 |
27 | Add the `fcsa-number` attribute to textboxes you want to have validated and formatted with thousands separators.
28 |
29 |
30 |
31 | When an invalid number is entered by the user, the form and control will become invalid and the 'fcsa-number-invalid' class will be added to the text box.
32 |
33 | ## Options
34 |
35 | Without any options passed to it, fcsa-number will validate that the input is a valid number and will also add commas for the thousand separators.
36 |
37 |
38 | #### max
39 |
40 | Validates the number is not above the max number.
41 |
42 | fcsa-number="{ max: 100 }"
43 |
44 | * Valid: 100
45 | * Invalid: 101
46 |
47 | #### min
48 |
49 | Validates the number is not below the min number.
50 |
51 | fcsa-number="{ min: -5 }"
52 |
53 | * Valid: -5
54 | * Invalid: -6
55 |
56 | #### maxDecimals
57 |
58 | Validates the number does not have more than the specified number of decimals.
59 |
60 | fcsa-number="{ maxDecimals: 2 }"
61 |
62 | * Valid: 1.23
63 | * Invalid: 1.234
64 |
65 | #### maxDigits
66 |
67 | Validates the number does not have more than the specified number of digits.
68 |
69 | fcsa-number="{ maxDigits: 2 }"
70 |
71 | * Valid: 76
72 | * Invalid: 123
73 |
74 | #### preventInvalidInput
75 |
76 | By default users are allowed to enter invalid characters, and then the textbox is marked invalid.
77 | If you want to prevent users from entering invalid characters altogether, then use the `preventInvalidInput` option.
78 | If the user presses the 'a' key, the directive will catch it and prevent 'a' from being shown in the textbox.
79 |
80 | fcsa-number="{ preventInvalidInput: true }"
81 |
82 | #### prepend
83 |
84 | Prepends the specified text before the number.
85 |
86 | fcsa-number="{ prepend: '$' }"
87 |
88 | #### append
89 |
90 | Appends the specified text after the number.
91 |
92 | fcsa-number="{ append: '%' }"
93 |
94 | ## Default Options
95 |
96 | It's possible to set the options globally too. You do this by calling `fcsaNumberConfigProvider.setDefaultOptions()`
97 | inside a config function in your app.
98 |
99 | Here's a code example that sets the default options:
100 |
101 | ```javascript
102 | var app = angular.module('yourApp', ['fcsa-number']);
103 | app.config(['fcsaNumberConfigProvider', function(fcsaNumberConfigProvider) {
104 | fcsaNumberConfigProvider.setDefaultOptions({
105 | max: 100,
106 | min: 0
107 | });
108 | }]);
109 | ```
110 |
111 | The default options can be overridden locally by passing in an options object: `fcsa-number="{ max: 10 }"`
112 |
113 | ## Developing
114 |
115 | Grunt is used to compile the CoffeeScript files and run the tests. To get started run the following commands on the command line:
116 |
117 | // installs the required node modules
118 | npm install
119 |
120 | // installs the required bower components
121 | bower install
122 |
123 | Run the following command to automatically compile and run the unit and end to end tests whenever you make a change to a file.
124 |
125 | grunt karma:unit:start express:dev watch
126 |
127 | To just run the protractor tests, you can run the following command.
128 |
129 | grunt express:dev e2e
130 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-fcsa-number",
3 | "version": "1.5.3",
4 | "authors": [
5 | "Paul Yoder "
6 | ],
7 | "description": "An Angular directive that validates numbers and adds commas for the thousands separator.",
8 | "main": "src/fcsaNumber.js",
9 | "keywords": [
10 | "angular",
11 | "directive",
12 | "format",
13 | "number",
14 | "validate"
15 | ],
16 | "license": "Apache V2",
17 | "homepage": "https://github.com/FCSAmericaDev/angular-fcsa-number",
18 | "ignore": [
19 | "**/.*",
20 | "**/*.coffee",
21 | "node_modules",
22 | "bower_components",
23 | "test",
24 | "Gruntfile.coffee",
25 | "karma.conf.js",
26 | "package.json"
27 | ],
28 | "dependencies": {
29 | "angular": "~1.x"
30 | },
31 | "devDependencies": {
32 | "angular-mocks": "~1.x"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/e2e/fcsaNumber.e2e.coffee:
--------------------------------------------------------------------------------
1 | describe 'fcsaNumber', ->
2 |
3 | describe 'on blur', ->
4 | it 'adds the thousand separator', ->
5 | input = element(By.model('amount'))
6 | input.clear()
7 | input.sendKeys '1000\t'
8 | expect(input.getAttribute('value')).toBe '1,000'
9 |
10 | it 'does not add commas to the decimal portion', ->
11 | input = element(By.model('amount'))
12 | input.clear()
13 | input.sendKeys '1234.5678\t'
14 | expect(input.getAttribute('value')).toBe '1,234.5678'
15 |
16 | it 'removes the thousand separators on focus', ->
17 | input = element(By.model('amount'))
18 | input.clear()
19 | input.sendKeys '1000\t'
20 | input.click()
21 |
22 | expect(input.getAttribute('value')).toBe '1000'
23 |
--------------------------------------------------------------------------------
/e2e/fcsaNumber.e2e.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | describe('fcsaNumber', function() {
3 | describe('on blur', function() {
4 | it('adds the thousand separator', function() {
5 | var input;
6 | input = element(By.model('amount'));
7 | input.clear();
8 | input.sendKeys('1000\t');
9 | return expect(input.getAttribute('value')).toBe('1,000');
10 | });
11 | return it('does not add commas to the decimal portion', function() {
12 | var input;
13 | input = element(By.model('amount'));
14 | input.clear();
15 | input.sendKeys('1234.5678\t');
16 | return expect(input.getAttribute('value')).toBe('1,234.5678');
17 | });
18 | });
19 | return it('removes the thousand separators on focus', function() {
20 | var input;
21 | input = element(By.model('amount'));
22 | input.clear();
23 | input.sendKeys('1000\t');
24 | input.click();
25 | return expect(input.getAttribute('value')).toBe('1000');
26 | });
27 | });
28 |
29 | }).call(this);
30 |
--------------------------------------------------------------------------------
/e2e/protractor.config:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | specs: ['fcsaNumber.e2e.js'],
3 |
4 | baseUrl: 'http://localhost:3000',
5 | onPrepare: function () {
6 | browser.driver.manage().window().maximize();
7 | browser.get('');
8 | },
9 |
10 | capabilities: {
11 | 'browserName': 'chrome'
12 | },
13 |
14 | // Options to be passed to Jasmine-node.
15 | jasmineNodeOpts: {
16 | showColors: true,
17 | defaultTimeoutInterval: 30000
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/e2e/web/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var app = express();
3 |
4 | app.use('/', express.static(__dirname + '/public'));
5 |
6 | app.listen(3000);
7 |
--------------------------------------------------------------------------------
/e2e/web/public/fcsaNumber.js:
--------------------------------------------------------------------------------
1 | /*! angular-fcsa-number (version 1.5.3) 2014-10-17 */
2 | (function() {
3 | var fcsaNumberModule,
4 | __hasProp = {}.hasOwnProperty;
5 |
6 | fcsaNumberModule = angular.module('fcsa-number', []);
7 |
8 | fcsaNumberModule.directive('fcsaNumber', [
9 | 'fcsaNumberConfig', function(fcsaNumberConfig) {
10 | var addCommasToInteger, controlKeys, defaultOptions, getOptions, hasMultipleDecimals, isNotControlKey, isNotDigit, isNumber, makeIsValid, makeMaxDecimals, makeMaxDigits, makeMaxNumber, makeMinNumber;
11 | defaultOptions = fcsaNumberConfig.defaultOptions;
12 | getOptions = function(scope) {
13 | var option, options, value, _ref;
14 | options = angular.copy(defaultOptions);
15 | if (scope.options != null) {
16 | _ref = scope.$eval(scope.options);
17 | for (option in _ref) {
18 | if (!__hasProp.call(_ref, option)) continue;
19 | value = _ref[option];
20 | options[option] = value;
21 | }
22 | }
23 | return options;
24 | };
25 | isNumber = function(val) {
26 | return !isNaN(parseFloat(val)) && isFinite(val);
27 | };
28 | isNotDigit = function(which) {
29 | return which < 44 || which > 57 || which === 47;
30 | };
31 | controlKeys = [0, 8, 13];
32 | isNotControlKey = function(which) {
33 | return controlKeys.indexOf(which) === -1;
34 | };
35 | hasMultipleDecimals = function(val) {
36 | return (val != null) && val.toString().split('.').length > 2;
37 | };
38 | makeMaxDecimals = function(maxDecimals) {
39 | var regexString, validRegex;
40 | if (maxDecimals > 0) {
41 | regexString = "^-?\\d*\\.?\\d{0," + maxDecimals + "}$";
42 | } else {
43 | regexString = "^-?\\d*$";
44 | }
45 | validRegex = new RegExp(regexString);
46 | return function(val) {
47 | return validRegex.test(val);
48 | };
49 | };
50 | makeMaxNumber = function(maxNumber) {
51 | return function(val, number) {
52 | return number <= maxNumber;
53 | };
54 | };
55 | makeMinNumber = function(minNumber) {
56 | return function(val, number) {
57 | return number >= minNumber;
58 | };
59 | };
60 | makeMaxDigits = function(maxDigits) {
61 | var validRegex;
62 | validRegex = new RegExp("^-?\\d{0," + maxDigits + "}(\\.\\d*)?$");
63 | return function(val) {
64 | return validRegex.test(val);
65 | };
66 | };
67 | makeIsValid = function(options) {
68 | var validations;
69 | validations = [];
70 | if (options.maxDecimals != null) {
71 | validations.push(makeMaxDecimals(options.maxDecimals));
72 | }
73 | if (options.max != null) {
74 | validations.push(makeMaxNumber(options.max));
75 | }
76 | if (options.min != null) {
77 | validations.push(makeMinNumber(options.min));
78 | }
79 | if (options.maxDigits != null) {
80 | validations.push(makeMaxDigits(options.maxDigits));
81 | }
82 | return function(val) {
83 | var i, number, _i, _ref;
84 | if (!isNumber(val)) {
85 | return false;
86 | }
87 | if (hasMultipleDecimals(val)) {
88 | return false;
89 | }
90 | number = Number(val);
91 | for (i = _i = 0, _ref = validations.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
92 | if (!validations[i](val, number)) {
93 | return false;
94 | }
95 | }
96 | return true;
97 | };
98 | };
99 | addCommasToInteger = function(val) {
100 | var commas, decimals, wholeNumbers;
101 | decimals = val.indexOf('.') == -1 ? '' : val.replace(/^-?\d+(?=\.)/, '');
102 | wholeNumbers = val.replace(/(\.\d+)$/, '');
103 | commas = wholeNumbers.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
104 | return "" + commas + decimals;
105 | };
106 | return {
107 | restrict: 'A',
108 | require: 'ngModel',
109 | scope: {
110 | options: '@fcsaNumber'
111 | },
112 | link: function(scope, elem, attrs, ngModelCtrl) {
113 | var isValid, options;
114 | options = getOptions(scope);
115 | isValid = makeIsValid(options);
116 | ngModelCtrl.$parsers.unshift(function(viewVal) {
117 | var noCommasVal;
118 | noCommasVal = viewVal.replace(/,/g, '');
119 | if (isValid(noCommasVal) || !noCommasVal) {
120 | ngModelCtrl.$setValidity('fcsaNumber', true);
121 | return noCommasVal;
122 | } else {
123 | ngModelCtrl.$setValidity('fcsaNumber', false);
124 | return void 0;
125 | }
126 | });
127 | ngModelCtrl.$formatters.push(function(val) {
128 | if ((options.nullDisplay != null) && (!val || val === '')) {
129 | return options.nullDisplay;
130 | }
131 | if ((val == null) || !isValid(val)) {
132 | return val;
133 | }
134 | ngModelCtrl.$setValidity('fcsaNumber', true);
135 | val = addCommasToInteger(val.toString());
136 | if (options.prepend != null) {
137 | val = "" + options.prepend + val;
138 | }
139 | if (options.append != null) {
140 | val = "" + val + options.append;
141 | }
142 | return val;
143 | });
144 | elem.on('blur', function() {
145 | var formatter, viewValue, _i, _len, _ref;
146 | viewValue = ngModelCtrl.$modelValue;
147 | if ((viewValue == null) || !isValid(viewValue)) {
148 | return;
149 | }
150 | _ref = ngModelCtrl.$formatters;
151 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
152 | formatter = _ref[_i];
153 | viewValue = formatter(viewValue);
154 | }
155 | ngModelCtrl.$viewValue = viewValue;
156 | return ngModelCtrl.$render();
157 | });
158 | elem.on('focus', function() {
159 | var val;
160 | val = elem.val();
161 | if (options.prepend != null) {
162 | val = val.replace(options.prepend, '');
163 | }
164 | if (options.append != null) {
165 | val = val.replace(options.append, '');
166 | }
167 | elem.val(val.replace(/,/g, ''));
168 | return elem[0].select();
169 | });
170 | if (options.preventInvalidInput === true) {
171 | return elem.on('keypress', function(e) {
172 | if (isNotDigit(e.which) && isNotControlKey(e.which)) {
173 | return e.preventDefault();
174 | }
175 | });
176 | }
177 | }
178 | };
179 | }
180 | ]);
181 |
182 | fcsaNumberModule.provider('fcsaNumberConfig', function() {
183 | var _defaultOptions;
184 | _defaultOptions = {};
185 | this.setDefaultOptions = function(defaultOptions) {
186 | return _defaultOptions = defaultOptions;
187 | };
188 | this.$get = function() {
189 | return {
190 | defaultOptions: _defaultOptions
191 | };
192 | };
193 | });
194 |
195 | }).call(this);
196 |
--------------------------------------------------------------------------------
/e2e/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Mon Jun 23 2014 13:06:50 GMT-0500 (Central Daylight Time)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'bower_components/angular/angular.js',
19 | 'bower_components/angular-mocks/angular-mocks.js',
20 | 'src/fcsaNumber.js',
21 | 'test/fcsaNumber.spec.js',
22 | 'test/fcsaNumberConfig.spec.js'
23 | ],
24 |
25 |
26 | // list of files to exclude
27 | exclude: [
28 |
29 | ],
30 |
31 |
32 | // preprocess matching files before serving them to the browser
33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
34 | preprocessors: {
35 |
36 | },
37 |
38 |
39 | // test results reporter to use
40 | // possible values: 'dots', 'progress'
41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
42 | reporters: ['progress'],
43 |
44 |
45 | // web server port
46 | port: 9876,
47 |
48 |
49 | // enable / disable colors in the output (reporters and logs)
50 | colors: true,
51 |
52 |
53 | // level of logging
54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
55 | logLevel: config.LOG_INFO,
56 |
57 |
58 | // enable / disable watching file and executing tests whenever any file changes
59 | autoWatch: false,
60 |
61 |
62 | // start these browsers
63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
64 | browsers: ['Chrome'],
65 |
66 |
67 | // Continuous Integration mode
68 | // if true, Karma captures browsers, runs the tests and exits
69 | singleRun: false
70 | });
71 | };
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-fcsa-number",
3 | "version": "1.5.3",
4 | "description": "An Angular directive that validates numbers and adds commas for the thousands separator.",
5 | "author": "https://github.com/FCSAmericaDev/fcsa-number/graphs/contributors",
6 | "license": "MIT",
7 | "homepage": "https://github.com/FCSAmericaDev/fcsa-number",
8 | "main": "src/fcsaNumber.js",
9 | "dependencies": {},
10 | "devDependencies": {
11 | "grunt": "0.4.5",
12 | "grunt-karma": "0.8.3",
13 | "karma": "^0.12.0",
14 | "karma-chrome-launcher": "^0.1.2",
15 | "matchdep": "0.3.0",
16 | "grunt-contrib-uglify": "~0.2.5",
17 | "grunt-contrib-coffee": "~0.7.0",
18 | "grunt-contrib-watch": "~0.5.3",
19 | "grunt-contrib-copy": "~0.5",
20 | "express": "~4.x",
21 | "grunt-express-server": "~0.4.17",
22 | "grunt-shell": "~0.7.0",
23 | "grunt-file-append": "0.0.5"
24 | },
25 | "scripts": {
26 | "test": "grunt --verbose"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/fcsaNumber.coffee:
--------------------------------------------------------------------------------
1 | fcsaNumberModule = angular.module('fcsa-number', [])
2 |
3 | fcsaNumberModule.directive 'fcsaNumber',
4 | ['fcsaNumberConfig', (fcsaNumberConfig) ->
5 |
6 | defaultOptions = fcsaNumberConfig.defaultOptions
7 |
8 | getOptions = (scope) ->
9 | options = angular.copy defaultOptions
10 | if scope.options?
11 | for own option, value of scope.$eval(scope.options)
12 | options[option] = value
13 | options
14 |
15 | isNumber = (val) ->
16 | !isNaN(parseFloat(val)) && isFinite(val)
17 |
18 | # 44 is ',', 45 is '-', 57 is '9' and 47 is '/'
19 | isNotDigit = (which) ->
20 | (which < 44 || which > 57 || which is 47)
21 |
22 | controlKeys = [0,8,13] # 0 = tab, 8 = backspace , 13 = enter
23 | isNotControlKey = (which) ->
24 | controlKeys.indexOf(which) == -1
25 |
26 | hasMultipleDecimals = (val) ->
27 | val? && val.toString().split('.').length > 2
28 |
29 | makeMaxDecimals = (maxDecimals) ->
30 | if maxDecimals > 0
31 | regexString = "^-?\\d*\\.?\\d{0,#{maxDecimals}}$"
32 | else
33 | regexString = "^-?\\d*$"
34 | validRegex = new RegExp regexString
35 |
36 | (val) -> validRegex.test val
37 |
38 | makeMaxNumber = (maxNumber) ->
39 | (val, number) -> number <= maxNumber
40 |
41 | makeMinNumber = (minNumber) ->
42 | (val, number) -> number >= minNumber
43 |
44 | makeMaxDigits = (maxDigits) ->
45 | validRegex = new RegExp "^-?\\d{0,#{maxDigits}}(\\.\\d*)?$"
46 | (val) -> validRegex.test val
47 |
48 | makeIsValid = (options) ->
49 | validations = []
50 |
51 | if options.maxDecimals?
52 | validations.push makeMaxDecimals options.maxDecimals
53 | if options.max?
54 | validations.push makeMaxNumber options.max
55 | if options.min?
56 | validations.push makeMinNumber options.min
57 | if options.maxDigits?
58 | validations.push makeMaxDigits options.maxDigits
59 |
60 | (val) ->
61 | return false unless isNumber val
62 | return false if hasMultipleDecimals val
63 | number = Number val
64 | for i in [0...validations.length]
65 | return false unless validations[i] val, number
66 | true
67 |
68 | addCommasToInteger = (val) ->
69 | decimals = `val.indexOf('.') == -1 ? '' : val.replace(/^-?\d+(?=\.)/, '')`
70 | wholeNumbers = val.replace /(\.\d+)$/, ''
71 | commas = wholeNumbers.replace /(\d)(?=(\d{3})+(?!\d))/g, '$1,'
72 | "#{commas}#{decimals}"
73 |
74 | {
75 | restrict: 'A'
76 | require: 'ngModel'
77 | scope:
78 | options: '@fcsaNumber'
79 | link: (scope, elem, attrs, ngModelCtrl) ->
80 | options = getOptions scope
81 | isValid = makeIsValid options
82 |
83 | ngModelCtrl.$parsers.unshift (viewVal) ->
84 | noCommasVal = viewVal.replace /,/g, ''
85 | if isValid(noCommasVal) || !noCommasVal
86 | ngModelCtrl.$setValidity 'fcsaNumber', true
87 | return noCommasVal
88 | else
89 | ngModelCtrl.$setValidity 'fcsaNumber', false
90 | return undefined
91 |
92 | ngModelCtrl.$formatters.push (val) ->
93 | if options.nullDisplay? && (!val || val == '')
94 | return options.nullDisplay
95 | return val if !val? || !isValid val
96 | ngModelCtrl.$setValidity 'fcsaNumber', true
97 | val = addCommasToInteger val.toString()
98 | if options.prepend?
99 | val = "#{options.prepend}#{val}"
100 | if options.append?
101 | val = "#{val}#{options.append}"
102 | val
103 |
104 | elem.on 'blur', ->
105 | viewValue = ngModelCtrl.$modelValue
106 | return if !viewValue? || !isValid(viewValue)
107 | for formatter in ngModelCtrl.$formatters
108 | viewValue = formatter(viewValue)
109 | ngModelCtrl.$viewValue = viewValue
110 | ngModelCtrl.$render()
111 |
112 | elem.on 'focus', ->
113 | val = elem.val()
114 | if options.prepend?
115 | val = val.replace options.prepend, ''
116 | if options.append?
117 | val = val.replace options.append, ''
118 | elem.val val.replace /,/g, ''
119 | elem[0].select()
120 |
121 | if options.preventInvalidInput == true
122 | elem.on 'keypress', (e) ->
123 | e.preventDefault() if isNotDigit(e.which) && isNotControlKey(e.which)
124 | }
125 | ]
126 |
127 | fcsaNumberModule.provider 'fcsaNumberConfig', ->
128 | _defaultOptions = {}
129 |
130 | @setDefaultOptions = (defaultOptions) ->
131 | _defaultOptions = defaultOptions
132 |
133 | @$get = ->
134 | defaultOptions: _defaultOptions
135 |
136 | return
137 |
--------------------------------------------------------------------------------
/src/fcsaNumber.js:
--------------------------------------------------------------------------------
1 | /*! angular-fcsa-number (version 1.5.3) 2014-10-17 */
2 | (function() {
3 | var fcsaNumberModule,
4 | __hasProp = {}.hasOwnProperty;
5 |
6 | fcsaNumberModule = angular.module('fcsa-number', []);
7 |
8 | fcsaNumberModule.directive('fcsaNumber', [
9 | 'fcsaNumberConfig', function(fcsaNumberConfig) {
10 | var addCommasToInteger, controlKeys, defaultOptions, getOptions, hasMultipleDecimals, isNotControlKey, isNotDigit, isNumber, makeIsValid, makeMaxDecimals, makeMaxDigits, makeMaxNumber, makeMinNumber;
11 | defaultOptions = fcsaNumberConfig.defaultOptions;
12 | getOptions = function(scope) {
13 | var option, options, value, _ref;
14 | options = angular.copy(defaultOptions);
15 | if (scope.options != null) {
16 | _ref = scope.$eval(scope.options);
17 | for (option in _ref) {
18 | if (!__hasProp.call(_ref, option)) continue;
19 | value = _ref[option];
20 | options[option] = value;
21 | }
22 | }
23 | return options;
24 | };
25 | isNumber = function(val) {
26 | return !isNaN(parseFloat(val)) && isFinite(val);
27 | };
28 | isNotDigit = function(which) {
29 | return which < 44 || which > 57 || which === 47;
30 | };
31 | controlKeys = [0, 8, 13];
32 | isNotControlKey = function(which) {
33 | return controlKeys.indexOf(which) === -1;
34 | };
35 | hasMultipleDecimals = function(val) {
36 | return (val != null) && val.toString().split('.').length > 2;
37 | };
38 | makeMaxDecimals = function(maxDecimals) {
39 | var regexString, validRegex;
40 | if (maxDecimals > 0) {
41 | regexString = "^-?\\d*\\.?\\d{0," + maxDecimals + "}$";
42 | } else {
43 | regexString = "^-?\\d*$";
44 | }
45 | validRegex = new RegExp(regexString);
46 | return function(val) {
47 | return validRegex.test(val);
48 | };
49 | };
50 | makeMaxNumber = function(maxNumber) {
51 | return function(val, number) {
52 | return number <= maxNumber;
53 | };
54 | };
55 | makeMinNumber = function(minNumber) {
56 | return function(val, number) {
57 | return number >= minNumber;
58 | };
59 | };
60 | makeMaxDigits = function(maxDigits) {
61 | var validRegex;
62 | validRegex = new RegExp("^-?\\d{0," + maxDigits + "}(\\.\\d*)?$");
63 | return function(val) {
64 | return validRegex.test(val);
65 | };
66 | };
67 | makeIsValid = function(options) {
68 | var validations;
69 | validations = [];
70 | if (options.maxDecimals != null) {
71 | validations.push(makeMaxDecimals(options.maxDecimals));
72 | }
73 | if (options.max != null) {
74 | validations.push(makeMaxNumber(options.max));
75 | }
76 | if (options.min != null) {
77 | validations.push(makeMinNumber(options.min));
78 | }
79 | if (options.maxDigits != null) {
80 | validations.push(makeMaxDigits(options.maxDigits));
81 | }
82 | return function(val) {
83 | var i, number, _i, _ref;
84 | if (!isNumber(val)) {
85 | return false;
86 | }
87 | if (hasMultipleDecimals(val)) {
88 | return false;
89 | }
90 | number = Number(val);
91 | for (i = _i = 0, _ref = validations.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
92 | if (!validations[i](val, number)) {
93 | return false;
94 | }
95 | }
96 | return true;
97 | };
98 | };
99 | addCommasToInteger = function(val) {
100 | var commas, decimals, wholeNumbers;
101 | decimals = val.indexOf('.') == -1 ? '' : val.replace(/^-?\d+(?=\.)/, '');
102 | wholeNumbers = val.replace(/(\.\d+)$/, '');
103 | commas = wholeNumbers.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
104 | return "" + commas + decimals;
105 | };
106 | return {
107 | restrict: 'A',
108 | require: 'ngModel',
109 | scope: {
110 | options: '@fcsaNumber'
111 | },
112 | link: function(scope, elem, attrs, ngModelCtrl) {
113 | var isValid, options;
114 | options = getOptions(scope);
115 | isValid = makeIsValid(options);
116 | ngModelCtrl.$parsers.unshift(function(viewVal) {
117 | var noCommasVal;
118 | noCommasVal = viewVal.replace(/,/g, '');
119 | if (isValid(noCommasVal) || !noCommasVal) {
120 | ngModelCtrl.$setValidity('fcsaNumber', true);
121 | return noCommasVal;
122 | } else {
123 | ngModelCtrl.$setValidity('fcsaNumber', false);
124 | return void 0;
125 | }
126 | });
127 | ngModelCtrl.$formatters.push(function(val) {
128 | if ((options.nullDisplay != null) && (!val || val === '')) {
129 | return options.nullDisplay;
130 | }
131 | if ((val == null) || !isValid(val)) {
132 | return val;
133 | }
134 | ngModelCtrl.$setValidity('fcsaNumber', true);
135 | val = addCommasToInteger(val.toString());
136 | if (options.prepend != null) {
137 | val = "" + options.prepend + val;
138 | }
139 | if (options.append != null) {
140 | val = "" + val + options.append;
141 | }
142 | return val;
143 | });
144 | elem.on('blur', function() {
145 | var formatter, viewValue, _i, _len, _ref;
146 | viewValue = ngModelCtrl.$modelValue;
147 | if ((viewValue == null) || !isValid(viewValue)) {
148 | return;
149 | }
150 | _ref = ngModelCtrl.$formatters;
151 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
152 | formatter = _ref[_i];
153 | viewValue = formatter(viewValue);
154 | }
155 | ngModelCtrl.$viewValue = viewValue;
156 | return ngModelCtrl.$render();
157 | });
158 | elem.on('focus', function() {
159 | var val;
160 | val = elem.val();
161 | if (options.prepend != null) {
162 | val = val.replace(options.prepend, '');
163 | }
164 | if (options.append != null) {
165 | val = val.replace(options.append, '');
166 | }
167 | elem.val(val.replace(/,/g, ''));
168 | return elem[0].select();
169 | });
170 | if (options.preventInvalidInput === true) {
171 | return elem.on('keypress', function(e) {
172 | if (isNotDigit(e.which) && isNotControlKey(e.which)) {
173 | return e.preventDefault();
174 | }
175 | });
176 | }
177 | }
178 | };
179 | }
180 | ]);
181 |
182 | fcsaNumberModule.provider('fcsaNumberConfig', function() {
183 | var _defaultOptions;
184 | _defaultOptions = {};
185 | this.setDefaultOptions = function(defaultOptions) {
186 | return _defaultOptions = defaultOptions;
187 | };
188 | this.$get = function() {
189 | return {
190 | defaultOptions: _defaultOptions
191 | };
192 | };
193 | });
194 |
195 | }).call(this);
196 |
--------------------------------------------------------------------------------
/src/fcsaNumber.min.js:
--------------------------------------------------------------------------------
1 | /*! angular-fcsa-number (version 1.5.3) 2014-10-17 */
2 | (function(){var a,b={}.hasOwnProperty;a=angular.module("fcsa-number",[]),a.directive("fcsaNumber",["fcsaNumberConfig",function(a){var c,d,e,f,g,h,i,j,k,l,m,n,o;return e=a.defaultOptions,f=function(a){var c,d,f,g;if(d=angular.copy(e),null!=a.options){g=a.$eval(a.options);for(c in g)b.call(g,c)&&(f=g[c],d[c]=f)}return d},j=function(a){return!isNaN(parseFloat(a))&&isFinite(a)},i=function(a){return 44>a||a>57||47===a},d=[0,8,13],h=function(a){return-1===d.indexOf(a)},g=function(a){return null!=a&&a.toString().split(".").length>2},l=function(a){var b,c;return b=a>0?"^-?\\d*\\.?\\d{0,"+a+"}$":"^-?\\d*$",c=new RegExp(b),function(a){return c.test(a)}},n=function(a){return function(b,c){return a>=c}},o=function(a){return function(b,c){return c>=a}},m=function(a){var b;return b=new RegExp("^-?\\d{0,"+a+"}(\\.\\d*)?$"),function(a){return b.test(a)}},k=function(a){var b;return b=[],null!=a.maxDecimals&&b.push(l(a.maxDecimals)),null!=a.max&&b.push(n(a.max)),null!=a.min&&b.push(o(a.min)),null!=a.maxDigits&&b.push(m(a.maxDigits)),function(a){var c,d,e,f;if(!j(a))return!1;if(g(a))return!1;for(d=Number(a),c=e=0,f=b.length;f>=0?f>e:e>f;c=f>=0?++e:--e)if(!b[c](a,d))return!1;return!0}},c=function(a){var b,c,d;return c=-1==a.indexOf(".")?"":a.replace(/^-?\d+(?=\.)/,""),d=a.replace(/(\.\d+)$/,""),b=d.replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1,"),""+b+c},{restrict:"A",require:"ngModel",scope:{options:"@fcsaNumber"},link:function(a,b,d,e){var g,j;return j=f(a),g=k(j),e.$parsers.unshift(function(a){var b;return b=a.replace(/,/g,""),g(b)||!b?(e.$setValidity("fcsaNumber",!0),b):void e.$setValidity("fcsaNumber",!1)}),e.$formatters.push(function(a){return null==j.nullDisplay||a&&""!==a?null!=a&&g(a)?(e.$setValidity("fcsaNumber",!0),a=c(a.toString()),null!=j.prepend&&(a=""+j.prepend+a),null!=j.append&&(a=""+a+j.append),a):a:j.nullDisplay}),b.on("blur",function(){var a,b,c,d,f;if(b=e.$modelValue,null!=b&&g(b)){for(f=e.$formatters,c=0,d=f.length;d>c;c++)a=f[c],b=a(b);return e.$viewValue=b,e.$render()}}),b.on("focus",function(){var a;return a=b.val(),null!=j.prepend&&(a=a.replace(j.prepend,"")),null!=j.append&&(a=a.replace(j.append,"")),b.val(a.replace(/,/g,"")),b[0].select()}),j.preventInvalidInput===!0?b.on("keypress",function(a){return i(a.which)&&h(a.which)?a.preventDefault():void 0}):void 0}}}]),a.provider("fcsaNumberConfig",function(){var a;a={},this.setDefaultOptions=function(b){return a=b},this.$get=function(){return{defaultOptions:a}}})}).call(this);
--------------------------------------------------------------------------------
/test/fcsaNumber.spec.coffee:
--------------------------------------------------------------------------------
1 | describe 'fcsaNumber', ->
2 | form = undefined
3 | $scope = undefined
4 | $compile = undefined
5 |
6 | beforeEach module 'fcsa-number'
7 | beforeEach inject(($rootScope, _$compile_) ->
8 | $scope = $rootScope
9 | $compile = _$compile_
10 | $scope.model = { number: 0 }
11 | )
12 |
13 | compileForm = (options = '{}') ->
14 | $compile("")($scope)
15 | $scope.$digest()
16 | $scope.form
17 |
18 | isValid = (args) ->
19 | args.options ||= '{}'
20 | $compile("")($scope)
21 | $scope.$digest()
22 | $scope.form.number.$setViewValue args.val
23 | $scope.form.number.$valid
24 |
25 | describe 'on focus', ->
26 | it 'removes the commas', ->
27 | $scope.model.number = 1000
28 | el = $compile("")($scope)
29 | el = el[0]
30 | $scope.$digest()
31 | angular.element(document.body).append el
32 | angular.element(el).triggerHandler 'focus'
33 | expect(el.value).toBe '1000'
34 |
35 | describe 'on blur', ->
36 | it 'adds commas', ->
37 | $scope.model.number = 1000
38 | el = $compile("")($scope)
39 | el = el[0]
40 | $scope.$digest()
41 | angular.element(document.body).append el
42 | angular.element(el).triggerHandler 'focus'
43 | angular.element(el).triggerHandler 'blur'
44 | expect(el.value).toBe '1,000'
45 |
46 | describe 'with negative decimal number', ->
47 | it 'correctly formats it', ->
48 | $scope.model.number = -1000.2
49 | el = $compile("")($scope)
50 | el = el[0]
51 | $scope.$digest()
52 | angular.element(document.body).append el
53 | angular.element(el).triggerHandler 'focus'
54 | angular.element(el).triggerHandler 'blur'
55 | expect(el.value).toBe '-1,000.2'
56 |
57 | describe 'when more than 3 decimals', ->
58 | it 'does not add commas to the decimals', ->
59 | $scope.model.number = 1234.5678
60 | el = $compile("")($scope)
61 | el = el[0]
62 | $scope.$digest()
63 | angular.element(document.body).append el
64 | angular.element(el).triggerHandler 'focus'
65 | angular.element(el).triggerHandler 'blur'
66 | expect(el.value).toBe '1,234.5678'
67 |
68 | describe 'no options', ->
69 | it 'validates positive number', ->
70 | valid = isValid
71 | val: '1'
72 | expect(valid).toBe true
73 |
74 | it 'validates positive number with commas', ->
75 | valid = isValid
76 | val: '1,23'
77 | expect(valid).toBe true
78 |
79 | it 'validates negative number', ->
80 | valid = isValid
81 | val: '-1'
82 | expect(valid).toBe true
83 |
84 | it 'invalidates hyphen only', ->
85 | valid = isValid
86 | val: '-'
87 | expect(valid).toBe false
88 |
89 | it 'validates number with decimals', ->
90 | valid = isValid
91 | val: '1.1'
92 | expect(valid).toBe true
93 |
94 | it 'validates number with decimals and commas', ->
95 | valid = isValid
96 | val: '1,123,142.1'
97 | expect(valid).toBe true
98 |
99 | it 'validates number while ignoring extra commas', ->
100 | valid = isValid
101 | val: '1,1,23,1,4,2.1'
102 | expect(valid).toBe true
103 |
104 | it 'invalidates number with multiple decimals', ->
105 | valid = isValid
106 | val: '1.1.2'
107 | expect(valid).toBe false
108 |
109 | it 'invalidates non number', ->
110 | valid = isValid
111 | val: '1a'
112 | expect(valid).toBe false
113 |
114 | describe 'options', ->
115 | describe 'max', ->
116 | it 'validates numbers below or equal to max', ->
117 | valid = isValid
118 | options: '{ max: 100 }'
119 | val: '100'
120 | expect(valid).toBe true
121 |
122 | it 'invalidates numbers above max', ->
123 | valid = isValid
124 | options: '{ max: 100 }'
125 | val: '100.1'
126 | expect(valid).toBe false
127 |
128 | describe 'min', ->
129 | it 'validates numbers not below the min', ->
130 | valid = isValid
131 | options: '{ min: 0 }'
132 | val: '0'
133 | expect(valid).toBe true
134 |
135 | it 'invalidates numbers below the min', ->
136 | valid = isValid
137 | options: '{ min: 0 }'
138 | val: '-0.1'
139 | expect(valid).toBe false
140 |
141 | describe 'maxDigits', ->
142 | it 'validates positive numbers not above number of digits', ->
143 | valid = isValid
144 | options: '{ maxDigits: 2 }'
145 | val: '99'
146 | expect(valid).toBe true
147 |
148 | it 'invalidates positive numbers above number of digits', ->
149 | valid = isValid
150 | options: '{ maxDigits: 2 }'
151 | val: '999'
152 | expect(valid).toBe false
153 |
154 | it 'validates negative numbers not above number of digits', ->
155 | valid = isValid
156 | options: '{ maxDigits: 2 }'
157 | val: '-99'
158 | expect(valid).toBe true
159 |
160 | describe 'maxDecimals', ->
161 | it 'validates numbers without more decimals', ->
162 | valid = isValid
163 | options: '{ maxDecimals: 2 }'
164 | val: '1.23'
165 | expect(valid).toBe true
166 |
167 | it 'invalidates numbers with more decimals', ->
168 | valid = isValid
169 | options: '{ maxDecimals: 2 }'
170 | val: '1.234'
171 | expect(valid).toBe false
172 |
173 | describe 'prepend', ->
174 | it 'prepends the value', ->
175 | $scope.model.number = 1000
176 | form = compileForm "{ prepend: \"$\" }"
177 | expect(form.number.$viewValue).toBe '$1,000'
178 | expect($scope.model.number).toBe 1000
179 |
180 | it 'removes the prepend value on focus', ->
181 | $scope.model.number = 1000
182 | el = $compile("")($scope)
183 | el = el[0]
184 | $scope.$digest()
185 | angular.element(document.body).append el
186 | angular.element(el).triggerHandler 'focus'
187 | expect(el.value).toBe '1000'
188 |
189 | describe 'append', ->
190 | it 'appends the value', ->
191 | $scope.model.number = 100
192 | form = compileForm "{ append: \"%\" }"
193 | expect(form.number.$viewValue).toBe '100%'
194 | expect($scope.model.number).toBe 100
195 |
196 | it 'removes the append value on focus', ->
197 | $scope.model.number = 100
198 | el = $compile("")($scope)
199 | el = el[0]
200 | $scope.$digest()
201 | angular.element(document.body).append el
202 | angular.element(el).triggerHandler 'focus'
203 | expect(el.value).toBe '100'
204 |
--------------------------------------------------------------------------------
/test/fcsaNumber.spec.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | describe('fcsaNumber', function() {
3 | var $compile, $scope, compileForm, form, isValid;
4 | form = void 0;
5 | $scope = void 0;
6 | $compile = void 0;
7 | beforeEach(module('fcsa-number'));
8 | beforeEach(inject(function($rootScope, _$compile_) {
9 | $scope = $rootScope;
10 | $compile = _$compile_;
11 | return $scope.model = {
12 | number: 0
13 | };
14 | }));
15 | compileForm = function(options) {
16 | if (options == null) {
17 | options = '{}';
18 | }
19 | $compile("")($scope);
20 | $scope.$digest();
21 | return $scope.form;
22 | };
23 | isValid = function(args) {
24 | args.options || (args.options = '{}');
25 | $compile("")($scope);
26 | $scope.$digest();
27 | $scope.form.number.$setViewValue(args.val);
28 | return $scope.form.number.$valid;
29 | };
30 | describe('on focus', function() {
31 | return it('removes the commas', function() {
32 | var el;
33 | $scope.model.number = 1000;
34 | el = $compile("")($scope);
35 | el = el[0];
36 | $scope.$digest();
37 | angular.element(document.body).append(el);
38 | angular.element(el).triggerHandler('focus');
39 | return expect(el.value).toBe('1000');
40 | });
41 | });
42 | describe('on blur', function() {
43 | it('adds commas', function() {
44 | var el;
45 | $scope.model.number = 1000;
46 | el = $compile("")($scope);
47 | el = el[0];
48 | $scope.$digest();
49 | angular.element(document.body).append(el);
50 | angular.element(el).triggerHandler('focus');
51 | angular.element(el).triggerHandler('blur');
52 | return expect(el.value).toBe('1,000');
53 | });
54 | describe('with negative decimal number', function() {
55 | return it('correctly formats it', function() {
56 | var el;
57 | $scope.model.number = -1000.2;
58 | el = $compile("")($scope);
59 | el = el[0];
60 | $scope.$digest();
61 | angular.element(document.body).append(el);
62 | angular.element(el).triggerHandler('focus');
63 | angular.element(el).triggerHandler('blur');
64 | return expect(el.value).toBe('-1,000.2');
65 | });
66 | });
67 | return describe('when more than 3 decimals', function() {
68 | return it('does not add commas to the decimals', function() {
69 | var el;
70 | $scope.model.number = 1234.5678;
71 | el = $compile("")($scope);
72 | el = el[0];
73 | $scope.$digest();
74 | angular.element(document.body).append(el);
75 | angular.element(el).triggerHandler('focus');
76 | angular.element(el).triggerHandler('blur');
77 | return expect(el.value).toBe('1,234.5678');
78 | });
79 | });
80 | });
81 | describe('no options', function() {
82 | it('validates positive number', function() {
83 | var valid;
84 | valid = isValid({
85 | val: '1'
86 | });
87 | return expect(valid).toBe(true);
88 | });
89 | it('validates positive number with commas', function() {
90 | var valid;
91 | valid = isValid({
92 | val: '1,23'
93 | });
94 | return expect(valid).toBe(true);
95 | });
96 | it('validates negative number', function() {
97 | var valid;
98 | valid = isValid({
99 | val: '-1'
100 | });
101 | return expect(valid).toBe(true);
102 | });
103 | it('invalidates hyphen only', function() {
104 | var valid;
105 | valid = isValid({
106 | val: '-'
107 | });
108 | return expect(valid).toBe(false);
109 | });
110 | it('validates number with decimals', function() {
111 | var valid;
112 | valid = isValid({
113 | val: '1.1'
114 | });
115 | return expect(valid).toBe(true);
116 | });
117 | it('validates number with decimals and commas', function() {
118 | var valid;
119 | valid = isValid({
120 | val: '1,123,142.1'
121 | });
122 | return expect(valid).toBe(true);
123 | });
124 | it('validates number while ignoring extra commas', function() {
125 | var valid;
126 | valid = isValid({
127 | val: '1,1,23,1,4,2.1'
128 | });
129 | return expect(valid).toBe(true);
130 | });
131 | it('invalidates number with multiple decimals', function() {
132 | var valid;
133 | valid = isValid({
134 | val: '1.1.2'
135 | });
136 | return expect(valid).toBe(false);
137 | });
138 | return it('invalidates non number', function() {
139 | var valid;
140 | valid = isValid({
141 | val: '1a'
142 | });
143 | return expect(valid).toBe(false);
144 | });
145 | });
146 | return describe('options', function() {
147 | describe('max', function() {
148 | it('validates numbers below or equal to max', function() {
149 | var valid;
150 | valid = isValid({
151 | options: '{ max: 100 }',
152 | val: '100'
153 | });
154 | return expect(valid).toBe(true);
155 | });
156 | return it('invalidates numbers above max', function() {
157 | var valid;
158 | valid = isValid({
159 | options: '{ max: 100 }',
160 | val: '100.1'
161 | });
162 | return expect(valid).toBe(false);
163 | });
164 | });
165 | describe('min', function() {
166 | it('validates numbers not below the min', function() {
167 | var valid;
168 | valid = isValid({
169 | options: '{ min: 0 }',
170 | val: '0'
171 | });
172 | return expect(valid).toBe(true);
173 | });
174 | return it('invalidates numbers below the min', function() {
175 | var valid;
176 | valid = isValid({
177 | options: '{ min: 0 }',
178 | val: '-0.1'
179 | });
180 | return expect(valid).toBe(false);
181 | });
182 | });
183 | describe('maxDigits', function() {
184 | it('validates positive numbers not above number of digits', function() {
185 | var valid;
186 | valid = isValid({
187 | options: '{ maxDigits: 2 }',
188 | val: '99'
189 | });
190 | return expect(valid).toBe(true);
191 | });
192 | it('invalidates positive numbers above number of digits', function() {
193 | var valid;
194 | valid = isValid({
195 | options: '{ maxDigits: 2 }',
196 | val: '999'
197 | });
198 | return expect(valid).toBe(false);
199 | });
200 | return it('validates negative numbers not above number of digits', function() {
201 | var valid;
202 | valid = isValid({
203 | options: '{ maxDigits: 2 }',
204 | val: '-99'
205 | });
206 | return expect(valid).toBe(true);
207 | });
208 | });
209 | describe('maxDecimals', function() {
210 | it('validates numbers without more decimals', function() {
211 | var valid;
212 | valid = isValid({
213 | options: '{ maxDecimals: 2 }',
214 | val: '1.23'
215 | });
216 | return expect(valid).toBe(true);
217 | });
218 | return it('invalidates numbers with more decimals', function() {
219 | var valid;
220 | valid = isValid({
221 | options: '{ maxDecimals: 2 }',
222 | val: '1.234'
223 | });
224 | return expect(valid).toBe(false);
225 | });
226 | });
227 | describe('prepend', function() {
228 | it('prepends the value', function() {
229 | $scope.model.number = 1000;
230 | form = compileForm("{ prepend: \"$\" }");
231 | expect(form.number.$viewValue).toBe('$1,000');
232 | return expect($scope.model.number).toBe(1000);
233 | });
234 | return it('removes the prepend value on focus', function() {
235 | var el;
236 | $scope.model.number = 1000;
237 | el = $compile("")($scope);
238 | el = el[0];
239 | $scope.$digest();
240 | angular.element(document.body).append(el);
241 | angular.element(el).triggerHandler('focus');
242 | return expect(el.value).toBe('1000');
243 | });
244 | });
245 | return describe('append', function() {
246 | it('appends the value', function() {
247 | $scope.model.number = 100;
248 | form = compileForm("{ append: \"%\" }");
249 | expect(form.number.$viewValue).toBe('100%');
250 | return expect($scope.model.number).toBe(100);
251 | });
252 | return it('removes the append value on focus', function() {
253 | var el;
254 | $scope.model.number = 100;
255 | el = $compile("")($scope);
256 | el = el[0];
257 | $scope.$digest();
258 | angular.element(document.body).append(el);
259 | angular.element(el).triggerHandler('focus');
260 | return expect(el.value).toBe('100');
261 | });
262 | });
263 | });
264 | });
265 |
266 | }).call(this);
267 |
--------------------------------------------------------------------------------
/test/fcsaNumberConfig.spec.coffee:
--------------------------------------------------------------------------------
1 | describe 'fcsaNumberConfig', ->
2 | $scope = undefined
3 | $compile = undefined
4 |
5 | beforeEach ->
6 | testModule = angular.module 'testModule', []
7 | testModule.config (fcsaNumberConfigProvider) ->
8 | fcsaNumberConfigProvider.setDefaultOptions {
9 | max: 1000
10 | min: 0
11 | }
12 |
13 | module 'fcsa-number', 'testModule'
14 |
15 | inject(($rootScope, _$compile_) ->
16 | $scope = $rootScope
17 | $compile = _$compile_
18 | $scope.model = { number: 0 }
19 | )
20 |
21 | isValid = (args) ->
22 | args.options ||= '{}'
23 | $compile("")($scope)
24 | $scope.$digest()
25 | $scope.form.number.$setViewValue args.val
26 | $scope.form.number.$valid
27 |
28 | it 'default max', ->
29 | valid = isValid
30 | val: '1001'
31 | expect(valid).toBe false
32 |
33 | it 'override max', ->
34 | valid = isValid
35 | options: '{ max: 10000 }'
36 | val: '1001'
37 | expect(valid).toBe true
38 |
39 | it 'default min', ->
40 | valid = isValid
41 | val: '-1'
42 | expect(valid).toBe false
43 |
44 | it 'override min', ->
45 | valid = isValid
46 | options: '{ min: -10 }'
47 | val: '-1'
48 | expect(valid).toBe true
49 |
50 | # Not going to test the rest of the options because the code structure
51 | # is such that if one default option works, then all the default options work
52 |
--------------------------------------------------------------------------------
/test/fcsaNumberConfig.spec.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | describe('fcsaNumberConfig', function() {
3 | var $compile, $scope, isValid;
4 | $scope = void 0;
5 | $compile = void 0;
6 | beforeEach(function() {
7 | var testModule;
8 | testModule = angular.module('testModule', []);
9 | testModule.config(function(fcsaNumberConfigProvider) {
10 | return fcsaNumberConfigProvider.setDefaultOptions({
11 | max: 1000,
12 | min: 0
13 | });
14 | });
15 | module('fcsa-number', 'testModule');
16 | return inject(function($rootScope, _$compile_) {
17 | $scope = $rootScope;
18 | $compile = _$compile_;
19 | return $scope.model = {
20 | number: 0
21 | };
22 | });
23 | });
24 | isValid = function(args) {
25 | args.options || (args.options = '{}');
26 | $compile("")($scope);
27 | $scope.$digest();
28 | $scope.form.number.$setViewValue(args.val);
29 | return $scope.form.number.$valid;
30 | };
31 | it('default max', function() {
32 | var valid;
33 | valid = isValid({
34 | val: '1001'
35 | });
36 | return expect(valid).toBe(false);
37 | });
38 | it('override max', function() {
39 | var valid;
40 | valid = isValid({
41 | options: '{ max: 10000 }',
42 | val: '1001'
43 | });
44 | return expect(valid).toBe(true);
45 | });
46 | it('default min', function() {
47 | var valid;
48 | valid = isValid({
49 | val: '-1'
50 | });
51 | return expect(valid).toBe(false);
52 | });
53 | return it('override min', function() {
54 | var valid;
55 | valid = isValid({
56 | options: '{ min: -10 }',
57 | val: '-1'
58 | });
59 | return expect(valid).toBe(true);
60 | });
61 | });
62 |
63 | }).call(this);
64 |
--------------------------------------------------------------------------------