├── .npmrc ├── .gitignore ├── docs ├── assets │ ├── css │ │ ├── docs.css │ │ └── old.css │ └── js │ │ └── docs.js ├── dist │ ├── input-spinner.min.css │ ├── input-spinner.css │ ├── input-spinner.min.css.map │ ├── input-spinner.min.js │ └── input-spinner.js ├── example.html └── index.html ├── .github └── workflows │ └── test.yml ├── tests ├── index.html ├── karma.conf.js └── unit │ └── unit.js ├── CONTRIBUTING.md ├── LICENSE ├── src ├── input-spinner.less └── input-spinner.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-prepublish=true 2 | lockfile-version=3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /*-dist.zip 4 | -------------------------------------------------------------------------------- /docs/assets/css/docs.css: -------------------------------------------------------------------------------- 1 | .gap-2 { 2 | gap: 0.5rem; 3 | } 4 | 5 | @property --stargazers { 6 | syntax: ""; 7 | initial-value: 0; 8 | inherits: false; 9 | } 10 | 11 | .stargazers-count { 12 | transition: --stargazers 1s; 13 | counter-reset: stargazers var(--stargazers); 14 | } 15 | 16 | .stargazers-count::after { 17 | content: counter(stargazers); 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run: 7 | name: Run tests 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Clone repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 22 18 | 19 | - name: Install npm dependencies 20 | run: npm ci 21 | 22 | - name: Run tests 23 | run: npm run test 24 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Test Suite 14 | 15 | 16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // process.env.CHROME_BIN = require('puppeteer').executablePath(); 2 | 3 | const conf = { 4 | browsers: [ 5 | 'ChromeHeadless' 6 | // 'ChromeHeadlessCustom' 7 | ], 8 | // customLaunchers: { 9 | // ChromeHeadlessCustom: { 10 | // base: 'ChromiumHeadless', 11 | // flags: [ 12 | // '--no-sandbox', 13 | // '--headless', 14 | // '--disable-gpu', 15 | // '--disable-translate', 16 | // '--disable-extensions' 17 | // ] 18 | // } 19 | // }, 20 | basePath: '..', 21 | colors: true, 22 | frameworks: [ 23 | 'qunit' 24 | ], 25 | plugins: [ 26 | 'karma-qunit', 27 | 'karma-chrome-launcher' 28 | ], 29 | files: [ 30 | 'node_modules/jquery/dist/jquery.slim.js', 31 | 'src/input-spinner.js', 32 | 'tests/unit/unit.js' 33 | ], 34 | autoWatch: false, 35 | singleRun: true, 36 | concurrency: Infinity 37 | }; 38 | 39 | module.exports = (config) => { 40 | config.set(conf); 41 | }; 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Important notes 4 | Please don't edit files in the `dist` subdirectory as they are generated via Grunt. You'll find source code in the `src` subdirectory! 5 | 6 | ### Code style 7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.** 8 | 9 | ## Modifying the code 10 | First, ensure that you have the latest [Node.js](https://nodejs.org) and [npm](https://npmjs.com) installed. 11 | 12 | 13 | 1. Fork and clone the repo 14 | 1. Run `npm install` to install all dependencies 15 | 1. Run `npm run test` 16 | 17 | ## Submitting pull requests 18 | 19 | 1. Create a new branch, please don't work in your `master` branch directly. 20 | 1. Add failing tests for the change you want to make. Run `npm run test` to see the tests fail. 21 | 1. Fix stuff. 22 | 1. Run `npm run test` to see if the tests pass. Repeat steps 2-4 until done. 23 | 1. Open `tests/index.html` unit test file(s) in actual browser to ensure tests pass everywhere. 24 | 1. Update the documentation to reflect any changes. 25 | 1. Push to your fork and submit a pull request. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Vasilii A., 2015-2018 4 | Copyright xixilive, 2013-2015 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/assets/js/docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var exec = function() { 4 | fetch('https://api.github.com/repos/vsn4ik/input-spinner') 5 | .then(function(response) { 6 | return response.json(); 7 | }) 8 | .then(function(response) { 9 | // NOTE: XSS check 10 | if (typeof response.stargazers_count !== 'number') { 11 | return; 12 | } 13 | 14 | document.querySelector('.js-stargazers-count').style.setProperty('--stargazers', response.stargazers_count); 15 | }); 16 | 17 | $('#customize-spinner').spinner('changed', function(event, newVal, oldVal) { 18 | $('#old-val').text(oldVal); 19 | $('#new-val').text(newVal); 20 | }); 21 | 22 | $('[data-trigger="spinner"]').spinner('changing', function(event, newVal, oldVal) { 23 | $(event.currentTarget).closest('.well').find('small').text('Old = ' + oldVal + ', New = ' + newVal); 24 | }); 25 | 26 | $('#step-spinner').spinner({ 27 | step: function(dir) { 28 | if ((this.oldValue === 1 && dir === 'down') || (this.oldValue === -1 && dir === 'up')) { 29 | return 2; 30 | } 31 | return 1; 32 | } 33 | }); 34 | }; 35 | 36 | if (document.readyState === 'loading') { 37 | document.addEventListener('DOMContentLoaded', exec); 38 | } else { 39 | exec(); 40 | } -------------------------------------------------------------------------------- /src/input-spinner.less: -------------------------------------------------------------------------------- 1 | /* bootstrap@4 */ 2 | .input-group.spinner .input-group-text { 3 | flex-direction: column; 4 | justify-content: center; 5 | padding-top: 0; 6 | padding-bottom: 0; 7 | 8 | .spin-up, 9 | .spin-down { 10 | display: flex; 11 | background: none; 12 | border: none; 13 | padding: 0; 14 | color: #999; 15 | } 16 | 17 | .spin-up:hover, 18 | .spin-down:hover, 19 | .spin-up:focus, 20 | .spin-down:focus { 21 | color: #555; 22 | } 23 | } 24 | 25 | /* bootstrap@3 */ 26 | .spinner.input-group .input-group-addon { 27 | .spin-up, 28 | .spin-down { 29 | height: 10px; 30 | width: 10px; 31 | overflow: hidden; 32 | display: block; 33 | text-align: center; 34 | color: #999; 35 | 36 | &:hover { 37 | color: #555; 38 | } 39 | 40 | .fas { 41 | margin-top: -6px; 42 | vertical-align: middle; 43 | } 44 | 45 | .glyphicon { 46 | font-size: 10px; 47 | top: -2px; 48 | } 49 | } 50 | 51 | a.spin-up, 52 | a.spin-down { 53 | text-decoration: none; 54 | } 55 | 56 | button.spin-up, 57 | button.spin-down { 58 | background: none; 59 | border: none; 60 | padding: 0; 61 | } 62 | } 63 | 64 | .spinner.input-group.input-group-sm .input-group-addon { 65 | .spin-up, 66 | .spin-down { 67 | height: 8px; 68 | 69 | .fas { 70 | margin-top: -10px; 71 | } 72 | 73 | .glyphicon { 74 | font-size: 8px; 75 | top: -5px; 76 | } 77 | } 78 | } 79 | 80 | .spinner.input-group.input-group-lg .input-group-addon { 81 | .spin-up, 82 | .spin-down { 83 | height: 12px; 84 | width: 12px; 85 | 86 | .fas { 87 | margin-top: -16px; 88 | } 89 | 90 | .glyphicon { 91 | font-size: 12px; 92 | top: -6px; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "input-spinner", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A Number-Spinner, Support keyboard operations and continuous changing", 6 | "keywords": [ 7 | "spinner", 8 | "number-steps", 9 | "number-spinner", 10 | "continuous" 11 | ], 12 | "homepage": "https://vsn4ik.github.io/input-spinner/", 13 | "repository": "vsn4ik/input-spinner", 14 | "license": "MIT", 15 | "author": { 16 | "name": "xixilive", 17 | "email": "xixilive@gmail.com", 18 | "url": "https://github.com/xixilive" 19 | }, 20 | "contributors": [ 21 | { 22 | "name": "Vasilii A.", 23 | "url": "https://github.com/vsn4ik" 24 | } 25 | ], 26 | "main": "dist/input-spinner.js", 27 | "files": [ 28 | "dist", 29 | "src" 30 | ], 31 | "scripts": { 32 | "css": "npm run css-compile; npm run css-minify", 33 | "css-compile": "lessc src/$npm_package_name.less dist/$npm_package_name.css", 34 | "css-minify": "cleancss --source-map --source-map-inline-sources --output dist/$npm_package_name.min.css dist/$npm_package_name.css", 35 | "js": "npm run js-compile; npm run js-minify", 36 | "js-compile": "mkdir dist; cp -rf src/*.js dist/", 37 | "js-minify": "terser --compress --output dist/$npm_package_name.min.js dist/$npm_package_name.js", 38 | "release-zip": "rm -rf $npm_package_name-$npm_package_version-dist && cp -r dist/ $npm_package_name-$npm_package_version-dist && zip -r9 $npm_package_name-$npm_package_version-dist.zip $npm_package_name-$npm_package_version-dist && rm -rf $npm_package_name-$npm_package_version-dist", 39 | "test": "npm run css; npm run js; karma start tests/karma.conf.js", 40 | "serve": "serve --listen 5000 --no-clipboard ./docs/" 41 | }, 42 | "devDependencies": { 43 | "clean-css-cli": "^5.6.2", 44 | "jquery": "^3.5.1", 45 | "karma": "^6.4.1", 46 | "karma-chrome-launcher": "^3.1.0", 47 | "karma-qunit": "^4.1.1", 48 | "less": "^4.1.3", 49 | "puppeteer": "^19.7.2", 50 | "qunit": "^2.10.1", 51 | "serve": "^14.2.1", 52 | "terser": "^5.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/dist/input-spinner.min.css: -------------------------------------------------------------------------------- 1 | .input-group.spinner .input-group-text{flex-direction:column;justify-content:center;padding-top:0;padding-bottom:0}.input-group.spinner .input-group-text .spin-down,.input-group.spinner .input-group-text .spin-up{display:flex;background:0 0;border:none;padding:0;color:#999}.input-group.spinner .input-group-text .spin-down:focus,.input-group.spinner .input-group-text .spin-down:hover,.input-group.spinner .input-group-text .spin-up:focus,.input-group.spinner .input-group-text .spin-up:hover{color:#555}.spinner.input-group .input-group-addon .spin-down,.spinner.input-group .input-group-addon .spin-up{height:10px;width:10px;overflow:hidden;display:block;text-align:center;color:#999}.spinner.input-group .input-group-addon .spin-down:hover,.spinner.input-group .input-group-addon .spin-up:hover{color:#555}.spinner.input-group .input-group-addon .spin-down .fas,.spinner.input-group .input-group-addon .spin-up .fas{margin-top:-6px;vertical-align:middle}.spinner.input-group .input-group-addon .spin-down .glyphicon,.spinner.input-group .input-group-addon .spin-up .glyphicon{font-size:10px;top:-2px}.spinner.input-group .input-group-addon a.spin-down,.spinner.input-group .input-group-addon a.spin-up{text-decoration:none}.spinner.input-group .input-group-addon button.spin-down,.spinner.input-group .input-group-addon button.spin-up{background:0 0;border:none;padding:0}.spinner.input-group.input-group-sm .input-group-addon .spin-down,.spinner.input-group.input-group-sm .input-group-addon .spin-up{height:8px}.spinner.input-group.input-group-sm .input-group-addon .spin-down .fas,.spinner.input-group.input-group-sm .input-group-addon .spin-up .fas{margin-top:-10px}.spinner.input-group.input-group-sm .input-group-addon .spin-down .glyphicon,.spinner.input-group.input-group-sm .input-group-addon .spin-up .glyphicon{font-size:8px;top:-5px}.spinner.input-group.input-group-lg .input-group-addon .spin-down,.spinner.input-group.input-group-lg .input-group-addon .spin-up{height:12px;width:12px}.spinner.input-group.input-group-lg .input-group-addon .spin-down .fas,.spinner.input-group.input-group-lg .input-group-addon .spin-up .fas{margin-top:-16px}.spinner.input-group.input-group-lg .input-group-addon .spin-down .glyphicon,.spinner.input-group.input-group-lg .input-group-addon .spin-up .glyphicon{font-size:12px;top:-6px} 2 | /*# sourceMappingURL=input-spinner.min.css.map */ -------------------------------------------------------------------------------- /docs/dist/input-spinner.css: -------------------------------------------------------------------------------- 1 | /* bootstrap@4 */ 2 | .input-group.spinner .input-group-text { 3 | flex-direction: column; 4 | justify-content: center; 5 | padding-top: 0; 6 | padding-bottom: 0; 7 | } 8 | .input-group.spinner .input-group-text .spin-up, 9 | .input-group.spinner .input-group-text .spin-down { 10 | display: flex; 11 | background: none; 12 | border: none; 13 | padding: 0; 14 | color: #999; 15 | } 16 | .input-group.spinner .input-group-text .spin-up:hover, 17 | .input-group.spinner .input-group-text .spin-down:hover, 18 | .input-group.spinner .input-group-text .spin-up:focus, 19 | .input-group.spinner .input-group-text .spin-down:focus { 20 | color: #555; 21 | } 22 | /* bootstrap@3 */ 23 | .spinner.input-group .input-group-addon .spin-up, 24 | .spinner.input-group .input-group-addon .spin-down { 25 | height: 10px; 26 | width: 10px; 27 | overflow: hidden; 28 | display: block; 29 | text-align: center; 30 | color: #999; 31 | } 32 | .spinner.input-group .input-group-addon .spin-up:hover, 33 | .spinner.input-group .input-group-addon .spin-down:hover { 34 | color: #555; 35 | } 36 | .spinner.input-group .input-group-addon .spin-up .fas, 37 | .spinner.input-group .input-group-addon .spin-down .fas { 38 | margin-top: -6px; 39 | vertical-align: middle; 40 | } 41 | .spinner.input-group .input-group-addon .spin-up .glyphicon, 42 | .spinner.input-group .input-group-addon .spin-down .glyphicon { 43 | font-size: 10px; 44 | top: -2px; 45 | } 46 | .spinner.input-group .input-group-addon a.spin-up, 47 | .spinner.input-group .input-group-addon a.spin-down { 48 | text-decoration: none; 49 | } 50 | .spinner.input-group .input-group-addon button.spin-up, 51 | .spinner.input-group .input-group-addon button.spin-down { 52 | background: none; 53 | border: none; 54 | padding: 0; 55 | } 56 | .spinner.input-group.input-group-sm .input-group-addon .spin-up, 57 | .spinner.input-group.input-group-sm .input-group-addon .spin-down { 58 | height: 8px; 59 | } 60 | .spinner.input-group.input-group-sm .input-group-addon .spin-up .fas, 61 | .spinner.input-group.input-group-sm .input-group-addon .spin-down .fas { 62 | margin-top: -10px; 63 | } 64 | .spinner.input-group.input-group-sm .input-group-addon .spin-up .glyphicon, 65 | .spinner.input-group.input-group-sm .input-group-addon .spin-down .glyphicon { 66 | font-size: 8px; 67 | top: -5px; 68 | } 69 | .spinner.input-group.input-group-lg .input-group-addon .spin-up, 70 | .spinner.input-group.input-group-lg .input-group-addon .spin-down { 71 | height: 12px; 72 | width: 12px; 73 | } 74 | .spinner.input-group.input-group-lg .input-group-addon .spin-up .fas, 75 | .spinner.input-group.input-group-lg .input-group-addon .spin-down .fas { 76 | margin-top: -16px; 77 | } 78 | .spinner.input-group.input-group-lg .input-group-addon .spin-up .glyphicon, 79 | .spinner.input-group.input-group-lg .input-group-addon .spin-down .glyphicon { 80 | font-size: 12px; 81 | top: -6px; 82 | } 83 | -------------------------------------------------------------------------------- /docs/dist/input-spinner.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["dist/input-spinner.css"],"names":[],"mappings":"AACA,uCACE,eAAgB,OAChB,gBAAiB,OACjB,YAAa,EACb,eAAgB,EAGlB,kDADA,gDAEE,QAAS,KACT,WAAY,IACZ,OAAQ,KACR,QAAS,EACT,MAAO,KAKT,wDAFA,wDACA,sDAFA,sDAIE,MAAO,KAIT,mDADA,iDAEE,OAAQ,KACR,MAAO,KACP,SAAU,OACV,QAAS,MACT,WAAY,OACZ,MAAO,KAGT,yDADA,uDAEE,MAAO,KAGT,wDADA,sDAEE,WAAY,KACZ,eAAgB,OAGlB,8DADA,4DAEE,UAAW,KACX,IAAK,KAGP,oDADA,kDAEE,gBAAiB,KAGnB,yDADA,uDAEE,WAAY,IACZ,OAAQ,KACR,QAAS,EAGX,kEADA,gEAEE,OAAQ,IAGV,uEADA,qEAEE,WAAY,MAGd,6EADA,2EAEE,UAAW,IACX,IAAK,KAGP,kEADA,gEAEE,OAAQ,KACR,MAAO,KAGT,uEADA,qEAEE,WAAY,MAGd,6EADA,2EAEE,UAAW,KACX,IAAK","sourcesContent":["/* bootstrap@4 */\n.input-group.spinner .input-group-text {\n flex-direction: column;\n justify-content: center;\n padding-top: 0;\n padding-bottom: 0;\n}\n.input-group.spinner .input-group-text .spin-up,\n.input-group.spinner .input-group-text .spin-down {\n display: flex;\n background: none;\n border: none;\n padding: 0;\n color: #999;\n}\n.input-group.spinner .input-group-text .spin-up:hover,\n.input-group.spinner .input-group-text .spin-down:hover,\n.input-group.spinner .input-group-text .spin-up:focus,\n.input-group.spinner .input-group-text .spin-down:focus {\n color: #555;\n}\n/* bootstrap@3 */\n.spinner.input-group .input-group-addon .spin-up,\n.spinner.input-group .input-group-addon .spin-down {\n height: 10px;\n width: 10px;\n overflow: hidden;\n display: block;\n text-align: center;\n color: #999;\n}\n.spinner.input-group .input-group-addon .spin-up:hover,\n.spinner.input-group .input-group-addon .spin-down:hover {\n color: #555;\n}\n.spinner.input-group .input-group-addon .spin-up .fas,\n.spinner.input-group .input-group-addon .spin-down .fas {\n margin-top: -6px;\n vertical-align: middle;\n}\n.spinner.input-group .input-group-addon .spin-up .glyphicon,\n.spinner.input-group .input-group-addon .spin-down .glyphicon {\n font-size: 10px;\n top: -2px;\n}\n.spinner.input-group .input-group-addon a.spin-up,\n.spinner.input-group .input-group-addon a.spin-down {\n text-decoration: none;\n}\n.spinner.input-group .input-group-addon button.spin-up,\n.spinner.input-group .input-group-addon button.spin-down {\n background: none;\n border: none;\n padding: 0;\n}\n.spinner.input-group.input-group-sm .input-group-addon .spin-up,\n.spinner.input-group.input-group-sm .input-group-addon .spin-down {\n height: 8px;\n}\n.spinner.input-group.input-group-sm .input-group-addon .spin-up .fas,\n.spinner.input-group.input-group-sm .input-group-addon .spin-down .fas {\n margin-top: -10px;\n}\n.spinner.input-group.input-group-sm .input-group-addon .spin-up .glyphicon,\n.spinner.input-group.input-group-sm .input-group-addon .spin-down .glyphicon {\n font-size: 8px;\n top: -5px;\n}\n.spinner.input-group.input-group-lg .input-group-addon .spin-up,\n.spinner.input-group.input-group-lg .input-group-addon .spin-down {\n height: 12px;\n width: 12px;\n}\n.spinner.input-group.input-group-lg .input-group-addon .spin-up .fas,\n.spinner.input-group.input-group-lg .input-group-addon .spin-down .fas {\n margin-top: -16px;\n}\n.spinner.input-group.input-group-lg .input-group-addon .spin-up .glyphicon,\n.spinner.input-group.input-group-lg .input-group-addon .spin-down .glyphicon {\n font-size: 12px;\n top: -6px;\n}\n"]} -------------------------------------------------------------------------------- /tests/unit/unit.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | 'use strict'; 3 | 4 | QUnit.test('should be defined on jquery object', function(assert) { 5 | assert.expect(1); 6 | assert.ok($.fn.spinner, 'spinner method is defined'); 7 | }); 8 | 9 | QUnit.module('Spinner', { 10 | beforeEach: function() { 11 | this.$el = $(` 12 |
13 | 14 | 15 | 16 |
17 | `).spinner(); 18 | 19 | this.$spinUp = this.$el.find('[data-spin="up"]'); 20 | this.$spinDown = this.$el.find('[data-spin="down"]'); 21 | this.spinner = this.$el.data('spinner'); 22 | }, 23 | afterEach: function() { 24 | this.$el 25 | .spinner('delay', 500) 26 | .spinner('changed', null) 27 | .spinner('changing', null); 28 | 29 | this.spinner.spinning.$el.val(1); 30 | } 31 | }); 32 | 33 | QUnit.test('spinner#value', function(assert) { 34 | assert.expect(1); 35 | assert.ok(this.spinner.value() === 1); 36 | }); 37 | 38 | QUnit.test('Spinner#delay', function(assert) { 39 | assert.expect(2); 40 | assert.ok(this.spinner.constructor.delay === 600); 41 | this.$el.spinner('delay', 300, 'should plus 100'); 42 | assert.ok(this.spinner.constructor.delay === 400); 43 | }); 44 | 45 | QUnit.test('Spinner#changed', function(assert) { 46 | var x = this.spinner.value(); 47 | var y = 0; 48 | 49 | this.$el.spinner('delay', 50).spinner('changed', function(e, newVal) { 50 | y = newVal; 51 | }); 52 | this.$spinUp.trigger('click'); 53 | var done = assert.async(); 54 | setTimeout(function() { 55 | assert.ok(y === x + 1); 56 | done(); 57 | }, 200); 58 | }); 59 | 60 | QUnit.test('Spinner#changing', function(assert) { 61 | this.$el.spinner('changing', function(e, newVal) { 62 | assert.ok(newVal === 2); 63 | }); 64 | this.$spinUp.trigger('click'); 65 | }); 66 | 67 | QUnit.test('spin via mouse click', function(assert) { 68 | assert.ok(this.spinner.value() === 1); 69 | this.$spinUp.trigger('click'); 70 | assert.ok(this.spinner.value() === 2); 71 | this.$spinDown.trigger('click'); 72 | assert.ok(this.spinner.value() === 1); 73 | }); 74 | 75 | QUnit.test('pass step as a function', function(assert) { 76 | assert.ok(this.spinner.value() === 1); 77 | this.spinner.spinning.options.step = function(dir) { //to skip 0 78 | if ((this.oldValue === 1 && dir === 'down') || (this.oldValue === -1 && dir === 'up')) { 79 | return 2; 80 | } 81 | 82 | return 1; 83 | }; 84 | this.$spinDown.trigger('click'); 85 | assert.ok(this.spinner.value() === -1); 86 | this.$spinUp.trigger('click'); 87 | assert.ok(this.spinner.value() === 1); 88 | }); 89 | 90 | QUnit.test('no spinning on disabled input', function(assert) { 91 | this.spinner.spinning.$el.prop('disabled', true); 92 | assert.ok(this.spinner.value() === 1); 93 | this.$spinUp.trigger('click'); 94 | assert.ok(this.spinner.value() === 1); 95 | this.spinner.spinning.$el.val(2); 96 | this.$spinDown.trigger('click'); 97 | assert.ok(this.spinner.value() === 2); 98 | this.spinner.spinning.$el.prop('disabled', false); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /docs/dist/input-spinner.min.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(factory){"function"==typeof define&&define.amd?define(["jquery"],factory):"object"==typeof exports?module.exports=factory(require("jquery")):factory(jQuery)}((function($){var Spinner,Spinning=function($element,options){return this.$el=$element,this.options=Object.assign({},Spinning.rules.defaults,Spinning.rules[options.rule]||{},options),this.min=Number(this.options.min)||0,this.max=Number(this.options.max)||0,this.spinningTimer=null,this.$el.on({"focus.spinner":function(e){e.preventDefault(),$(this.$el).trigger("mouseup.spinner"),this.oldValue=this.value()}.bind(this),"change.spinner":function(e){e.preventDefault(),this.value(this.$el.val())}.bind(this),"keydown.spinner":function(e){var dir={38:"up",40:"down"}[e.keyCode];dir&&(e.preventDefault(),this.spin(dir))}.bind(this)}),this.oldValue=this.value(),this.value(this.$el.val()),this};Spinning.rules={defaults:{min:null,max:null,step:1,precision:0},currency:{min:0,max:null,step:.01,precision:2},quantity:{min:1,max:999,step:1,precision:0},percent:{min:1,max:100,step:1,precision:0},month:{min:1,max:12,step:1,precision:0},day:{min:1,max:31,step:1,precision:0},hour:{min:0,max:23,step:1,precision:0},minute:{min:1,max:59,step:1,precision:0},second:{min:1,max:59,step:1,precision:0}},Spinning.prototype={spin:function(dir){if(!this.$el.prop("disabled")){this.oldValue=this.value();var step="function"==typeof this.options.step?this.options.step.call(this,dir):this.options.step,multipler="up"===dir?1:-1;this.value(this.oldValue+Number(step)*multipler)}},value:function(v){if(null==v)return this.numeric(this.$el.val());v=this.numeric(v);var valid=this.validate(v);0!==valid&&(v=-1===valid?this.min:this.max),this.$el.val(v.toFixed(this.options.precision)),this.oldValue!==this.value()&&(this.$el.trigger("changing.spinner",[this.value(),this.oldValue]),clearTimeout(this.spinningTimer),this.spinningTimer=setTimeout(function(){this.$el.trigger("changed.spinner",[this.value(),this.oldValue])}.bind(this),Spinner.delay))},numeric:function(v){return v=this.options.precision>0?parseFloat(v,10):parseInt(v,10),isFinite(v)?v:v||this.options.min||0},validate:function(val){return null!==this.options.min&&valthis.max?1:0}},(Spinner=function(element,options){this.$el=$(element),this.$spinning=this.$el.find('[data-spin="spinner"]'),0===this.$spinning.length&&(this.$spinning=this.$el.find('input[type="text"]')),options=Object.assign({},options,this.$spinning.data()),this.spinning=new Spinning(this.$spinning,options),this.$el.on("click.spinner",'[data-spin="up"], [data-spin="down"]',this.spin.bind(this)).on("mousedown.spinner",'[data-spin="up"], [data-spin="down"]',this.spin.bind(this)).on("mouseup.spinner",'[data-spin="up"], [data-spin="down"]',this.spin.bind(this)),options.delay&&this.delay(options.delay),options.changed&&this.changed(options.changed),options.changing&&this.changing(options.changing)}).delay=500,Spinner.prototype={constructor:Spinner,spin:function(e){var dir=$(e.currentTarget).data("spin");switch(e.type){case"click":e.preventDefault(),this.spinning.spin(dir);break;case"mousedown":1===e.keyCode&&(this.spinTimeout=setTimeout(this.beginSpin.bind(this,dir),300));break;case"mouseup":clearTimeout(this.spinTimeout),clearInterval(this.spinInterval)}},delay:function(ms){var delay=Number(ms);delay>=0&&(this.constructor.delay=delay+100)},value:function(){return this.spinning.value()},changed:function(fn){this.bindHandler("changed.spinner",fn)},changing:function(fn){this.bindHandler("changing.spinner",fn)},bindHandler:function(t,fn){"function"==typeof fn?this.$spinning.on(t,fn):this.$spinning.off(t)},beginSpin:function(dir){this.spinInterval=setInterval(this.spinning.spin.bind(this.spinning,dir),100)}};var old=$.fn.spinner;return $.fn.spinner=function(options,value){return this.each((function(){var data=$.data(this,"spinner");data||(data=new Spinner(this,options),$.data(this,"spinner",data)),"delay"===options||"changed"===options||"changing"===options?data[options](value):"step"===options&&value?data.spinning.step=value:"spin"===options&&value&&data.spinning.spin(value)}))},$.fn.spinner.Constructor=Spinner,$.fn.spinner.noConflict=function(){return $.fn.spinner=old,this},$((function(){$('[data-trigger="spinner"]').spinner()})),$.fn.spinner})); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Input Spinner](https://vsn4ik.github.io/input-spinner/) 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![Build Status](https://github.com/vsn4ik/input-spinner/actions/workflows/test.yml/badge.svg)](https://github.com/vsn4ik/input-spinner/actions/workflows/test.yml) 5 | 6 | 7 | A Number-Spinner, Support keyboard operations and continuous changing. 8 | 9 | ## Basic usage, it's very simple 10 | ```html 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 |
20 | ``` 21 | 22 | ## Getting Started 23 | Download the [production version][production] or the [development version][development]. 24 | 25 | In your web page: 26 | 27 | ```html 28 | 29 | 30 | 40 | 41 |
42 | 43 | 44 | 45 |
46 | ``` 47 | 48 | ## Documentation 49 | ### Spinner options 50 | 51 | #### delay 52 | > delay to fire changed event in millisecond, default is 500. 53 | 54 | #### changed 55 | > changed event handler, the changed event is a lazy-mode event, default is null. 56 | 57 | #### changing 58 | > changing event handler, the changing event will be fired immediately, default is null. 59 | 60 | ### Spinning Options(setup via data-api) 61 | #### min 62 | > the minimum value, default is null. 63 | 64 | #### max 65 | > the maximum value, default is null. 66 | 67 | #### step 68 | > the changing-value of per-step, if passed as a function, the function will be called within the spinner object scope. 69 | 70 | #### precision 71 | > the precision of value 72 | 73 | ### Built-in rules 74 | ```javascript 75 | currency: { min: 0.00, max: null, step: 0.01, precision: 2 }, 76 | quantity: { min: 1, max: 999, step: 1, precision:0 }, 77 | percent: { min: 1, max: 100, step: 1, precision:0 }, 78 | month: { min: 1, max: 12, step: 1, precision:0 }, 79 | day: { min: 1, max: 31, step: 1, precision:0 }, 80 | hour: { min: 0, max: 23, step: 1, precision:0 }, 81 | minute: { min: 1, max: 59, step: 1, precision:0 }, 82 | second: { min: 1, max: 59, step: 1, precision:0 } 83 | ``` 84 | Usage: 85 | ```html 86 | 87 | ``` 88 | 89 | ## Examples 90 | 91 | ### Work with Bootstrap and Font Awesome 92 | 93 | ```html 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 |
103 | ``` 104 | 105 | ### Customize 106 | 107 | #### specify a field 108 | 109 | ```html 110 |
111 | 112 | 113 |
114 | ``` 115 | 116 | #### Use hidden field 117 | 118 | ```html 119 |
120 | 121 | 122 | - 123 | + 124 |
125 | 126 | 131 | ``` 132 | 133 | #### pass step options as a function 134 | ```javascript 135 | // To skip 0 136 | $('#spinner').spinner({ 137 | step: function(dir) { 138 | // 'this' references to the spinner object 139 | if ((this.oldValue === 1 && dir === 'down') || (this.oldValue === -1 && dir === 'up')) { 140 | return 2; 141 | } 142 | return 1; 143 | } 144 | }); 145 | 146 | // or use API syntax 147 | $('#spinner').spinner('step', function(dir) { 148 | // your logic here 149 | }); 150 | ``` 151 | 152 | 153 | ## Copyright and license 154 | 155 | Copyright Vasilii A., 2015–2018 156 | Copyright xixilive, 2013–2015 157 | 158 | Licensed under [the MIT License][license]. 159 | 160 | [license]: https://github.com/vsn4ik/input-spinner/blob/master/LICENSE 161 | [development]: https://raw.githubusercontent.com/vsn4ik/input-spinner/master/dist/input-spinner.min.js 162 | [production]: https://raw.githubusercontent.com/vsn4ik/input-spinner/master/dist/input-spinner.js 163 | -------------------------------------------------------------------------------- /docs/assets/css/old.css: -------------------------------------------------------------------------------- 1 | /*@import url(https://fonts.googleapis.com/css?family=Arvo:400,700,400italic);*/ 2 | 3 | /* MeyerWeb Reset */ 4 | 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font: inherit; 22 | vertical-align: baseline; 23 | } 24 | 25 | 26 | /* Base text styles */ 27 | 28 | body { 29 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 30 | font-size: 14px; 31 | color: #232323; 32 | background-color: #FBFAF7; 33 | line-height: 1.8em; 34 | -webkit-font-smoothing: antialiased; 35 | } 36 | 37 | h1, h2, h3, h4, h5, h6 { 38 | color: #232323; 39 | margin: 36px 0 10px; 40 | } 41 | 42 | header p, ul, ol, table, dl { 43 | margin: 0 0 22px; 44 | } 45 | 46 | h1, h2, h3 { 47 | font-family: Arvo, Monaco, serif; 48 | line-height: 1.3; 49 | font-weight: normal; 50 | } 51 | 52 | h1, h2, h3 { 53 | display: block; 54 | border-bottom: 1px solid #ccc; 55 | padding-bottom: 5px; 56 | } 57 | 58 | h1 { 59 | font-size: 30px; 60 | } 61 | 62 | h2 { 63 | font-size: 24px; 64 | } 65 | 66 | h3 { 67 | font-size: 18px; 68 | } 69 | 70 | h4, h5, h6 { 71 | font-family: Arvo, Monaco, serif; 72 | font-weight: 700; 73 | } 74 | 75 | a { 76 | color: #C30000; 77 | font-weight: 200; 78 | text-decoration: none; 79 | } 80 | 81 | a:hover { 82 | text-decoration: underline; 83 | } 84 | 85 | a small { 86 | font-size: 12px; 87 | } 88 | 89 | em { 90 | font-style: italic; 91 | } 92 | 93 | strong { 94 | font-weight: 700; 95 | } 96 | 97 | ul li { 98 | list-style: inside; 99 | padding-left: 25px; 100 | } 101 | 102 | ol li { 103 | list-style: decimal inside; 104 | padding-left: 20px; 105 | } 106 | 107 | blockquote { 108 | margin: 0; 109 | padding: 0 0 0 20px; 110 | font-style: italic; 111 | } 112 | 113 | dl, dt, dd, dl p { 114 | font-color: #444; 115 | } 116 | 117 | dl dt { 118 | font-weight: bold; 119 | } 120 | 121 | dl dd { 122 | padding-left: 20px; 123 | font-style: italic; 124 | } 125 | 126 | dl p { 127 | padding-left: 20px; 128 | font-style: italic; 129 | } 130 | 131 | hr { 132 | border: 0; 133 | background: #ccc; 134 | height: 1px; 135 | margin: 0 0 24px; 136 | } 137 | 138 | /* Tables */ 139 | 140 | table { 141 | width: 100%; 142 | } 143 | 144 | table { 145 | border: 1px solid #ccc; 146 | margin-bottom: 32px; 147 | text-align: left; 148 | } 149 | 150 | th { 151 | font-family: 'Arvo', Helvetica, Arial, sans-serif; 152 | font-size: 18px; 153 | font-weight: normal; 154 | padding: 10px; 155 | background: #232323; 156 | color: #FDFEFB; 157 | } 158 | 159 | td { 160 | padding: 10px; 161 | background: #ccc; 162 | } 163 | 164 | 165 | /* Wrapper */ 166 | .wrapper { 167 | display: flex; 168 | align-items: baseline; 169 | /* width: 960px; */ 170 | } 171 | 172 | 173 | /* Header */ 174 | 175 | header { 176 | background-color: #171717; 177 | color: #FDFDFB; 178 | width: 320px; 179 | position: sticky; 180 | top: 30px; 181 | border: 1px solid #000; 182 | border-top-right-radius: 4px; 183 | border-bottom-right-radius: 4px; 184 | padding: 34px 25px 22px 50px; 185 | margin: 30px 25px 0 0; 186 | -webkit-font-smoothing: antialiased; 187 | } 188 | 189 | p.header { 190 | font-size: 16px; 191 | } 192 | 193 | h1.header { 194 | font-family: Arvo, sans-serif; 195 | font-size: 30px; 196 | font-weight: 300; 197 | line-height: 1.3em; 198 | border-bottom: none; 199 | margin-top: 0; 200 | } 201 | 202 | header a, h1.header { 203 | color: #fff; 204 | } 205 | 206 | header ul { 207 | list-style: none; 208 | padding: 0; 209 | } 210 | 211 | header li { 212 | list-style-type: none; 213 | width: 132px; 214 | height: 15px; 215 | margin-bottom: 12px; 216 | line-height: 1em; 217 | padding: 6px 6px 6px 7px; 218 | 219 | background: #AF0011; 220 | background: linear-gradient(top, #AF0011 0%,#820011 100%); 221 | 222 | border-radius: 4px; 223 | border: 1px solid #0D0D0D; 224 | 225 | -webkit-box-shadow: inset 0px 1px 1px 0 rgba(233,2,38, 1); 226 | box-shadow: inset 0px 1px 1px 0 rgba(233,2,38, 1); 227 | } 228 | 229 | header li:hover { 230 | background: #C3001D; 231 | background: linear-gradient(top, #C3001D 0%,#950119 100%); 232 | } 233 | 234 | 235 | /* Section - for main page content */ 236 | 237 | section { 238 | width: 650px; 239 | padding-bottom: 50px; 240 | } 241 | 242 | @media print, screen and (max-width: 960px) { 243 | div.wrapper { 244 | flex-direction: column; 245 | } 246 | 247 | header, section { 248 | position: static; 249 | width: auto; 250 | } 251 | 252 | section { 253 | padding: 20px 84px 20px 50px; 254 | margin: 0 0 20px; 255 | } 256 | 257 | header a small { 258 | display: inline; 259 | } 260 | 261 | header ul { 262 | position: absolute; 263 | right: 130px; 264 | top: 84px; 265 | } 266 | } 267 | 268 | @media print, screen and (max-width: 720px) { 269 | body { 270 | word-wrap: break-word; 271 | } 272 | 273 | header { 274 | padding: 20px; 275 | margin-right: 0; 276 | } 277 | 278 | section { 279 | padding: 10px 0 10px 20px; 280 | margin: 0 0 30px; 281 | } 282 | 283 | header ul, header p.view { 284 | position: static; 285 | } 286 | } 287 | 288 | @media print, screen and (max-width: 480px) { 289 | header ul li.download { 290 | display: none; 291 | } 292 | } 293 | 294 | @media print { 295 | body { 296 | padding: 0.4in; 297 | font-size: 12pt; 298 | color: #444; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /docs/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Input Spinner - Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 |
26 |

Examples

27 |
    28 |
  • Arrow-up for increment
  • 29 |
  • Arrow-down for decrement
  • 30 |
  • Press Arrows or Left-mouse cause continuous changing
  • 31 |
32 | 33 |
34 |

Quantity:

35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 |

Currency:

49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |

Percent:

63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 | 75 |
76 |

Month:

77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 |
88 | 89 |
90 |

Customize (even number less than 200):

91 |
92 | 93 | 94 |
95 |
96 | 97 | 98 |
99 |
100 |
101 |

Event handler: changed

102 |

103 | Old = , New = 104 |

105 |
106 | 107 |
108 |

Disabled input:

109 |
110 | 111 | 112 |
113 |
114 | 115 | 116 |
117 |
118 |
119 |
120 | 121 |
122 |

Control sizing:

123 |
124 | 125 | 126 |
127 |
128 | 129 | 130 |
131 |
132 |
133 | 134 |
135 | 136 |
137 | 138 | 139 |
140 |
141 | 142 | 143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Input Spinner 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

Input Spinner

23 |

A Number-Spinner, Support keyboard operations and continuous changing.

24 | 25 |
26 | 27 | 28 | Download 29 | 30 | 31 |
32 | 38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |

Examples

50 |
    51 |
  • Arrow-up for increment
  • 52 |
  • Arrow-down for decrement
  • 53 |
  • Press Arrows or Left-mouse cause continuous changing
  • 54 |
55 | 56 |
57 |

Quantity:

58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 | 70 |
71 |

Currency:

72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 |
82 |
83 | 84 |
85 |

Percent:

86 |
87 | 88 | 89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 | 98 |
99 |

Month:

100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 |
111 | 112 |
113 |

Hour:

114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 |
123 |
124 |
125 | 126 |

Customize

127 |
128 |

even number less than 200

129 |
130 | 131 | 132 |
133 |
134 | 135 | 136 |
137 |
138 |
139 |

Event handler: changed

140 |

141 | Old = , New = 142 |

143 |
144 | 145 |
146 |

Pass step option as a function

147 |
To skip 0 for this example
148 |
149 | 150 | 151 |
152 |
153 | 154 | 155 |
156 |
157 |
158 |
159 |
160 |
161 | 162 | 163 | -------------------------------------------------------------------------------- /src/input-spinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * input-spinner 3 | * https://github.com/vsn4ik/input-spinner 4 | * Copyright Vasilii A., 2015–2018 5 | * Copyright xixilive, 2013–2015 6 | * Licensed under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | (function(factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | // AMD. Register as an anonymous module 14 | define(['jquery'], factory); 15 | } else if (typeof exports === 'object') { 16 | // Node/CommonJS 17 | module.exports = factory(require('jquery')); 18 | } else { 19 | // Browser globals 20 | factory(jQuery); 21 | } 22 | })(function($) { 23 | var Spinner; 24 | var Spinning = function($element, options) { 25 | this.$el = $element; 26 | this.options = Object.assign({}, Spinning.rules.defaults, Spinning.rules[options.rule] || {}, options); 27 | this.min = Number(this.options.min) || 0; 28 | this.max = Number(this.options.max) || 0; 29 | this.spinningTimer = null; 30 | 31 | this.$el.on({ 32 | 'focus.spinner': (function(e) { 33 | e.preventDefault(); 34 | $(this.$el).trigger('mouseup.spinner'); 35 | this.oldValue = this.value(); 36 | }).bind(this), 37 | 'change.spinner': (function(e) { 38 | e.preventDefault(); 39 | this.value(this.$el.val()); 40 | }).bind(this), 41 | 'keydown.spinner': (function(e) { 42 | var dir = { 43 | 38: 'up', 44 | 40: 'down' 45 | }[e.keyCode]; 46 | 47 | if (dir) { 48 | e.preventDefault(); 49 | this.spin(dir); 50 | } 51 | }).bind(this) 52 | }); 53 | 54 | //init input value 55 | this.oldValue = this.value(); 56 | this.value(this.$el.val()); 57 | return this; 58 | }; 59 | 60 | Spinning.rules = { 61 | defaults: { min: null, max: null, step: 1, precision: 0 }, 62 | currency: { min: 0.00, max: null, step: 0.01, precision: 2 }, 63 | quantity: { min: 1, max: 999, step: 1, precision: 0 }, 64 | percent: { min: 1, max: 100, step: 1, precision: 0 }, 65 | month: { min: 1, max: 12, step: 1, precision: 0 }, 66 | day: { min: 1, max: 31, step: 1, precision: 0 }, 67 | hour: { min: 0, max: 23, step: 1, precision: 0 }, 68 | minute: { min: 1, max: 59, step: 1, precision: 0 }, 69 | second: { min: 1, max: 59, step: 1, precision: 0 } 70 | }; 71 | 72 | Spinning.prototype = { 73 | spin: function(dir) { 74 | if (this.$el.prop('disabled')) { 75 | return; 76 | } 77 | 78 | this.oldValue = this.value(); 79 | var step = typeof this.options.step === 'function' ? this.options.step.call(this, dir) : this.options.step; 80 | var multipler = dir === 'up' ? 1 : -1; 81 | 82 | this.value(this.oldValue + Number(step) * multipler); 83 | }, 84 | 85 | value: function(v) { 86 | if (v === null || v === undefined) { 87 | return this.numeric(this.$el.val()); 88 | } 89 | v = this.numeric(v); 90 | 91 | var valid = this.validate(v); 92 | if (valid !== 0) { 93 | v = valid === -1 ? this.min : this.max; 94 | } 95 | this.$el.val(v.toFixed(this.options.precision)); 96 | 97 | if (this.oldValue !== this.value()) { 98 | // changing.spinner 99 | this.$el.trigger('changing.spinner', [this.value(), this.oldValue]); 100 | 101 | // lazy changed.spinner 102 | clearTimeout(this.spinningTimer); 103 | this.spinningTimer = setTimeout((function() { 104 | this.$el.trigger('changed.spinner', [this.value(), this.oldValue]); 105 | }).bind(this), Spinner.delay); 106 | } 107 | }, 108 | 109 | numeric: function(v) { 110 | v = this.options.precision > 0 ? parseFloat(v, 10) : parseInt(v, 10); 111 | 112 | // If the variable is a number 113 | if (isFinite(v)) { 114 | return v; 115 | } 116 | 117 | return v || this.options.min || 0; 118 | }, 119 | 120 | validate: function(val) { 121 | if (this.options.min !== null && val < this.min) { 122 | return -1; 123 | } 124 | 125 | if (this.options.max !== null && val > this.max) { 126 | return 1; 127 | } 128 | 129 | return 0; 130 | } 131 | }; 132 | 133 | Spinner = function(element, options) { 134 | this.$el = $(element); 135 | this.$spinning = this.$el.find('[data-spin="spinner"]'); 136 | 137 | if (this.$spinning.length === 0) { 138 | this.$spinning = this.$el.find('input[type="text"]'); 139 | } 140 | 141 | options = Object.assign({}, options, this.$spinning.data()); 142 | 143 | this.spinning = new Spinning(this.$spinning, options); 144 | 145 | this.$el 146 | .on('click.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)) 147 | .on('mousedown.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)) 148 | .on('mouseup.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)); 149 | 150 | if (options.delay) { 151 | this.delay(options.delay); 152 | } 153 | 154 | if (options.changed) { 155 | this.changed(options.changed); 156 | } 157 | 158 | if (options.changing) { 159 | this.changing(options.changing); 160 | } 161 | }; 162 | 163 | Spinner.delay = 500; 164 | 165 | Spinner.prototype = { 166 | constructor: Spinner, 167 | 168 | spin: function(e) { 169 | var dir = $(e.currentTarget).data('spin'); 170 | 171 | switch (e.type) { 172 | case 'click': 173 | e.preventDefault(); 174 | this.spinning.spin(dir); 175 | break; 176 | case 'mousedown': 177 | if (e.keyCode === 1) { 178 | this.spinTimeout = setTimeout(this.beginSpin.bind(this, dir), 300); 179 | } 180 | break; 181 | case 'mouseup': 182 | clearTimeout(this.spinTimeout); 183 | clearInterval(this.spinInterval); 184 | break; 185 | } 186 | }, 187 | 188 | delay: function(ms) { 189 | var delay = Number(ms); 190 | 191 | if (delay >= 0) { 192 | this.constructor.delay = delay + 100; 193 | } 194 | }, 195 | 196 | value: function() { 197 | return this.spinning.value(); 198 | }, 199 | 200 | changed: function(fn) { 201 | this.bindHandler('changed.spinner', fn); 202 | }, 203 | 204 | changing: function(fn) { 205 | this.bindHandler('changing.spinner', fn); 206 | }, 207 | 208 | bindHandler: function(t, fn) { 209 | if (typeof fn === 'function') { 210 | this.$spinning.on(t, fn); 211 | } else { 212 | this.$spinning.off(t); 213 | } 214 | }, 215 | 216 | beginSpin: function(dir) { 217 | this.spinInterval = setInterval(this.spinning.spin.bind(this.spinning, dir), 100); 218 | } 219 | }; 220 | 221 | var old = $.fn.spinner; 222 | 223 | $.fn.spinner = function(options, value) { 224 | return this.each(function() { 225 | var data = $.data(this, 'spinner'); 226 | 227 | if (!data) { 228 | data = new Spinner(this, options); 229 | 230 | $.data(this, 'spinner', data); 231 | } 232 | if (options === 'delay' || options === 'changed' || options === 'changing') { 233 | data[options](value); 234 | } else if (options === 'step' && value) { 235 | data.spinning.step = value; 236 | } else if (options === 'spin' && value) { 237 | data.spinning.spin(value); 238 | } 239 | }); 240 | }; 241 | 242 | $.fn.spinner.Constructor = Spinner; 243 | $.fn.spinner.noConflict = function() { 244 | $.fn.spinner = old; 245 | return this; 246 | }; 247 | 248 | $(function() { 249 | $('[data-trigger="spinner"]').spinner(); 250 | }); 251 | 252 | return $.fn.spinner; 253 | }); 254 | -------------------------------------------------------------------------------- /docs/dist/input-spinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * input-spinner 3 | * https://github.com/vsn4ik/input-spinner 4 | * Copyright Vasilii A., 2015–2018 5 | * Copyright xixilive, 2013–2015 6 | * Licensed under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | (function(factory) { 12 | if (typeof define === 'function' && define.amd) { 13 | // AMD. Register as an anonymous module 14 | define(['jquery'], factory); 15 | } else if (typeof exports === 'object') { 16 | // Node/CommonJS 17 | module.exports = factory(require('jquery')); 18 | } else { 19 | // Browser globals 20 | factory(jQuery); 21 | } 22 | })(function($) { 23 | var Spinner; 24 | var Spinning = function($element, options) { 25 | this.$el = $element; 26 | this.options = Object.assign({}, Spinning.rules.defaults, Spinning.rules[options.rule] || {}, options); 27 | this.min = Number(this.options.min) || 0; 28 | this.max = Number(this.options.max) || 0; 29 | this.spinningTimer = null; 30 | 31 | this.$el.on({ 32 | 'focus.spinner': (function(e) { 33 | e.preventDefault(); 34 | $(this.$el).trigger('mouseup.spinner'); 35 | this.oldValue = this.value(); 36 | }).bind(this), 37 | 'change.spinner': (function(e) { 38 | e.preventDefault(); 39 | this.value(this.$el.val()); 40 | }).bind(this), 41 | 'keydown.spinner': (function(e) { 42 | var dir = { 43 | 38: 'up', 44 | 40: 'down' 45 | }[e.keyCode]; 46 | 47 | if (dir) { 48 | e.preventDefault(); 49 | this.spin(dir); 50 | } 51 | }).bind(this) 52 | }); 53 | 54 | //init input value 55 | this.oldValue = this.value(); 56 | this.value(this.$el.val()); 57 | return this; 58 | }; 59 | 60 | Spinning.rules = { 61 | defaults: { min: null, max: null, step: 1, precision: 0 }, 62 | currency: { min: 0.00, max: null, step: 0.01, precision: 2 }, 63 | quantity: { min: 1, max: 999, step: 1, precision: 0 }, 64 | percent: { min: 1, max: 100, step: 1, precision: 0 }, 65 | month: { min: 1, max: 12, step: 1, precision: 0 }, 66 | day: { min: 1, max: 31, step: 1, precision: 0 }, 67 | hour: { min: 0, max: 23, step: 1, precision: 0 }, 68 | minute: { min: 1, max: 59, step: 1, precision: 0 }, 69 | second: { min: 1, max: 59, step: 1, precision: 0 } 70 | }; 71 | 72 | Spinning.prototype = { 73 | spin: function(dir) { 74 | if (this.$el.prop('disabled')) { 75 | return; 76 | } 77 | 78 | this.oldValue = this.value(); 79 | var step = typeof this.options.step === 'function' ? this.options.step.call(this, dir) : this.options.step; 80 | var multipler = dir === 'up' ? 1 : -1; 81 | 82 | this.value(this.oldValue + Number(step) * multipler); 83 | }, 84 | 85 | value: function(v) { 86 | if (v === null || v === undefined) { 87 | return this.numeric(this.$el.val()); 88 | } 89 | v = this.numeric(v); 90 | 91 | var valid = this.validate(v); 92 | if (valid !== 0) { 93 | v = valid === -1 ? this.min : this.max; 94 | } 95 | this.$el.val(v.toFixed(this.options.precision)); 96 | 97 | if (this.oldValue !== this.value()) { 98 | // changing.spinner 99 | this.$el.trigger('changing.spinner', [this.value(), this.oldValue]); 100 | 101 | // lazy changed.spinner 102 | clearTimeout(this.spinningTimer); 103 | this.spinningTimer = setTimeout((function() { 104 | this.$el.trigger('changed.spinner', [this.value(), this.oldValue]); 105 | }).bind(this), Spinner.delay); 106 | } 107 | }, 108 | 109 | numeric: function(v) { 110 | v = this.options.precision > 0 ? parseFloat(v, 10) : parseInt(v, 10); 111 | 112 | // If the variable is a number 113 | if (isFinite(v)) { 114 | return v; 115 | } 116 | 117 | return v || this.options.min || 0; 118 | }, 119 | 120 | validate: function(val) { 121 | if (this.options.min !== null && val < this.min) { 122 | return -1; 123 | } 124 | 125 | if (this.options.max !== null && val > this.max) { 126 | return 1; 127 | } 128 | 129 | return 0; 130 | } 131 | }; 132 | 133 | Spinner = function(element, options) { 134 | this.$el = $(element); 135 | this.$spinning = this.$el.find('[data-spin="spinner"]'); 136 | 137 | if (this.$spinning.length === 0) { 138 | this.$spinning = this.$el.find('input[type="text"]'); 139 | } 140 | 141 | options = Object.assign({}, options, this.$spinning.data()); 142 | 143 | this.spinning = new Spinning(this.$spinning, options); 144 | 145 | this.$el 146 | .on('click.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)) 147 | .on('mousedown.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)) 148 | .on('mouseup.spinner', '[data-spin="up"], [data-spin="down"]', this.spin.bind(this)); 149 | 150 | if (options.delay) { 151 | this.delay(options.delay); 152 | } 153 | 154 | if (options.changed) { 155 | this.changed(options.changed); 156 | } 157 | 158 | if (options.changing) { 159 | this.changing(options.changing); 160 | } 161 | }; 162 | 163 | Spinner.delay = 500; 164 | 165 | Spinner.prototype = { 166 | constructor: Spinner, 167 | 168 | spin: function(e) { 169 | var dir = $(e.currentTarget).data('spin'); 170 | 171 | switch (e.type) { 172 | case 'click': 173 | e.preventDefault(); 174 | this.spinning.spin(dir); 175 | break; 176 | case 'mousedown': 177 | if (e.keyCode === 1) { 178 | this.spinTimeout = setTimeout(this.beginSpin.bind(this, dir), 300); 179 | } 180 | break; 181 | case 'mouseup': 182 | clearTimeout(this.spinTimeout); 183 | clearInterval(this.spinInterval); 184 | break; 185 | } 186 | }, 187 | 188 | delay: function(ms) { 189 | var delay = Number(ms); 190 | 191 | if (delay >= 0) { 192 | this.constructor.delay = delay + 100; 193 | } 194 | }, 195 | 196 | value: function() { 197 | return this.spinning.value(); 198 | }, 199 | 200 | changed: function(fn) { 201 | this.bindHandler('changed.spinner', fn); 202 | }, 203 | 204 | changing: function(fn) { 205 | this.bindHandler('changing.spinner', fn); 206 | }, 207 | 208 | bindHandler: function(t, fn) { 209 | if (typeof fn === 'function') { 210 | this.$spinning.on(t, fn); 211 | } else { 212 | this.$spinning.off(t); 213 | } 214 | }, 215 | 216 | beginSpin: function(dir) { 217 | this.spinInterval = setInterval(this.spinning.spin.bind(this.spinning, dir), 100); 218 | } 219 | }; 220 | 221 | var old = $.fn.spinner; 222 | 223 | $.fn.spinner = function(options, value) { 224 | return this.each(function() { 225 | var data = $.data(this, 'spinner'); 226 | 227 | if (!data) { 228 | data = new Spinner(this, options); 229 | 230 | $.data(this, 'spinner', data); 231 | } 232 | if (options === 'delay' || options === 'changed' || options === 'changing') { 233 | data[options](value); 234 | } else if (options === 'step' && value) { 235 | data.spinning.step = value; 236 | } else if (options === 'spin' && value) { 237 | data.spinning.spin(value); 238 | } 239 | }); 240 | }; 241 | 242 | $.fn.spinner.Constructor = Spinner; 243 | $.fn.spinner.noConflict = function() { 244 | $.fn.spinner = old; 245 | return this; 246 | }; 247 | 248 | $(function() { 249 | $('[data-trigger="spinner"]').spinner(); 250 | }); 251 | 252 | return $.fn.spinner; 253 | }); 254 | --------------------------------------------------------------------------------