├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── run-tests.yml ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmrc ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── __tests__ ├── basicOperations.test.ts ├── events.test.ts ├── helpers │ ├── jestPuppeteerServerSetup.ts │ └── touchspinHelpers.ts └── html │ ├── index-bs3-vertical.html │ ├── index-bs3.html │ ├── index-bs4.html │ ├── index-vertical.html │ ├── rtl-bs3.html │ └── rtl-bs4.html ├── bower.json ├── demo ├── demo.css ├── favicon.ico ├── index-bs3.html └── index.html ├── dist ├── jquery.bootstrap-touchspin.css ├── jquery.bootstrap-touchspin.js ├── jquery.bootstrap-touchspin.js.map ├── jquery.bootstrap-touchspin.min.css └── jquery.bootstrap-touchspin.min.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── jquery.bootstrap-touchspin.css └── jquery.bootstrap-touchspin.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: istvan-ujjmeszaros 4 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | run-name: Run tests 3 | 4 | on: [push] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | 16 | - name: Cache npm dependencies 17 | uses: actions/cache@v3 18 | env: 19 | cache-name: cache-node-modules 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | ${{ runner.os }}-build-${{ env.cache-name }}- 25 | ${{ runner.os }}-build- 26 | ${{ runner.os }}- 27 | 28 | - name: Install dependencies 29 | if: steps.cache-nodemodules.outputs.cache-hit != 'true' 30 | run: npm ci 31 | 32 | - name: Run tests 33 | run: npm test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hg 2 | .hgignore 3 | .idea 4 | *.log 5 | nbproject 6 | index_demo_site.html 7 | node_modules 8 | bower_components 9 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 8, 3 | "boss": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "eqnull": true, 7 | "expr": true, 8 | "immed": true, 9 | "noarg": true, 10 | "onevar": false, 11 | "quotmark": "single", 12 | "smarttabs": true, 13 | "trailing": true, 14 | "unused": true, 15 | "node": true, 16 | "globals": { 17 | "$": true, 18 | "jQuery": true, 19 | "document": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | init.eol=lf 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before sending a pull request remember to follow [jQuery Core Style Guide](http://contribute.jquery.org/style-guide/js/). 4 | 5 | 1. Fork it! 6 | 2. Create your feature branch: `git checkout -b my-new-feature` 7 | 3. Make your changes on the `src` folder, never on the `dist` folder. 8 | 4. Commit your changes: `git commit -m 'Add some feature'` 9 | 5. Push to the branch: `git push origin my-new-feature` 10 | 6. Submit a pull request :D 11 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | module.exports = function (grunt) { 3 | const distFolder = 'dist/'; 4 | 5 | grunt.loadNpmTasks('grunt-contrib-concat'); 6 | grunt.loadNpmTasks('grunt-contrib-jshint'); 7 | grunt.loadNpmTasks('grunt-contrib-uglify'); 8 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 9 | grunt.loadNpmTasks('grunt-contrib-clean'); 10 | grunt.loadNpmTasks('grunt-babel'); 11 | 12 | grunt.registerTask('default', ['jshint', 'concat', 'babel', 'uglify', 'cssmin', 'update-license-version']); 13 | 14 | grunt.initConfig({ 15 | 16 | // Import package manifest 17 | pkg: grunt.file.readJSON('package.json'), 18 | pkg: grunt.file.readJSON('package.json'), 19 | 20 | // Banner definitions 21 | meta: { 22 | banner: '/*\n' + 23 | ' * <%= pkg.title || pkg.name %> - v<%= pkg.version %>\n' + 24 | ' * <%= pkg.description %>\n' + 25 | ' * <%= pkg.homepage %>\n' + 26 | ' *\n' + 27 | ' * Made by <%= pkg.author.name %>\n' + 28 | ' * Under <%= pkg.license %> License\n' + 29 | ' */\n' 30 | }, 31 | 32 | // Concat definitions 33 | concat: { 34 | js: { 35 | src: ['src/jquery.bootstrap-touchspin.js'], 36 | dest: 'dist/jquery.bootstrap-touchspin.js' 37 | }, 38 | css: { 39 | src: ['src/jquery.bootstrap-touchspin.css'], 40 | dest: 'dist/jquery.bootstrap-touchspin.css' 41 | }, 42 | options: { 43 | banner: '<%= meta.banner %>' 44 | } 45 | }, 46 | 47 | babel: { 48 | options: { 49 | sourceMap: true, 50 | presets: ['@babel/preset-env'] 51 | }, 52 | dist: { 53 | files: { 54 | 'dist/jquery.bootstrap-touchspin.js': 'dist/jquery.bootstrap-touchspin.js' 55 | } 56 | } 57 | }, 58 | 59 | // Lint definitions 60 | jshint: { 61 | files: ['src/**/*.js', '__tests__/**/*.js'], 62 | options: { 63 | jshintrc: '.jshintrc' 64 | } 65 | }, 66 | 67 | // Minify definitions 68 | uglify: { 69 | js: { 70 | src: ['dist/jquery.bootstrap-touchspin.js'], 71 | dest: 'dist/jquery.bootstrap-touchspin.min.js' 72 | }, 73 | options: { 74 | banner: '<%= meta.banner %>' 75 | } 76 | }, 77 | 78 | cssmin: { 79 | css: { 80 | src: ['dist/jquery.bootstrap-touchspin.css'], 81 | dest: 'dist/jquery.bootstrap-touchspin.min.css' 82 | }, 83 | options: { 84 | banner: '<%= meta.banner %>' 85 | } 86 | } 87 | }); 88 | 89 | // Clean task for dist folder 90 | grunt.config('clean', { 91 | folder: [distFolder + '**/*'], 92 | }); 93 | 94 | grunt.registerTask('update-license-version', 'Update the LICENSE.md file with the current version number', function() { 95 | const pkg = grunt.config.get('pkg'); 96 | const licenseTemplate = ` 97 | MIT License 98 | 99 | Bootstrap TouchSpin 100 | v${pkg.version} 101 | 102 | A mobile and touch friendly input spinner component for Bootstrap 3 & 4. 103 | 104 | https://github.com/istvan-ujjmeszaros/bootstrap-touchspin 105 | http://www.virtuosoft.eu/code/bootstrap-touchspin/ 106 | 107 | Copyright (c) 2013-${new Date().getFullYear()} István Ujj-Mészáros 108 | 109 | Permission is hereby granted, free of charge, to any person obtaining a copy 110 | of this software and associated documentation files (the "Software"), to deal 111 | in the Software without restriction, including without limitation the rights 112 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 113 | copies of the Software, and to permit persons to whom the Software is 114 | furnished to do so, subject to the following conditions: 115 | 116 | The above copyright notice and this permission notice shall be included in all 117 | copies or substantial portions of the Software. 118 | 119 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 120 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 121 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 122 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 123 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 124 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 125 | SOFTWARE. 126 | `; 127 | 128 | grunt.file.write('LICENSE.md', licenseTemplate); 129 | }); 130 | 131 | // Checking if the dist folder has been properly rebuilt with "grunt default" before pushing to GitHub 132 | grunt.registerTask('check-build-integrity', 'Build task with checksum verification', function() { 133 | const done = this.async(); 134 | const initialChecksum = calculateChecksum(distFolder); 135 | 136 | grunt.log.writeln('Initial checksum:', initialChecksum); 137 | 138 | // Clean dist folder 139 | grunt.util.spawn({ 140 | cmd: 'grunt', 141 | args: ['clean'], 142 | opts: {stdio: 'inherit'} 143 | }, function(error) { 144 | if (error) { 145 | grunt.fail.fatal('Error running "clean" task: ' + error); 146 | } else { 147 | grunt.util.spawn({ 148 | cmd: 'grunt', 149 | args: ['default'], 150 | opts: {stdio: 'inherit'} 151 | }, function(error) { 152 | if (error) { 153 | grunt.fail.fatal('Error running "default" task: ' + error); 154 | } else { 155 | const finalChecksum = calculateChecksum(distFolder); 156 | 157 | grunt.log.writeln('Final checksum:', finalChecksum); 158 | 159 | if (initialChecksum !== finalChecksum) { 160 | grunt.fail.fatal('Checksums do not match, please rebuild the dist files with "grunt default"!'); 161 | } else { 162 | grunt.log.ok('Checksums match, the dist folder is up-to-date!'); 163 | } 164 | } 165 | 166 | done(); 167 | }); 168 | } 169 | }); 170 | }); 171 | 172 | function calculateChecksum(folderPath) { 173 | const files = grunt.file.expand(folderPath + '**/*'); 174 | const hasher = crypto.createHash('md5'); 175 | 176 | files.forEach(function(filePath) { 177 | if (grunt.file.isFile(filePath)) { 178 | hasher.update(grunt.file.read(filePath)); 179 | } 180 | }); 181 | 182 | return hasher.digest('hex'); 183 | } 184 | 185 | }; 186 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Bootstrap TouchSpin 5 | v4.7.3 6 | 7 | A mobile and touch friendly input spinner component for Bootstrap 3 & 4. 8 | 9 | https://github.com/istvan-ujjmeszaros/bootstrap-touchspin 10 | http://www.virtuosoft.eu/code/bootstrap-touchspin/ 11 | 12 | Copyright (c) 2013-2024 István Ujj-Mészáros 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap TouchSpin [![run-tests](https://github.com/istvan-ujjmeszaros/bootstrap-touchspin/actions/workflows/run-tests.yml/badge.svg)](https://github.com/istvan-ujjmeszaros/bootstrap-touchspin/actions/workflows/run-tests.yml) 2 | 3 | ##### Bootstrap TouchSpin is a mobile and touch friendly input spinner component for Bootstrap 3 & 4. 4 | 5 | - [Website](http://www.virtuosoft.eu/code/bootstrap-touchspin/) 6 | 7 | Please report issues and feel free to make feature suggestions as well. 8 | 9 | ## License 10 | 11 | MIT License 12 | -------------------------------------------------------------------------------- /__tests__/basicOperations.test.ts: -------------------------------------------------------------------------------- 1 | import touchspinHelpers from './helpers/touchspinHelpers'; 2 | import {page, port} from './helpers/jestPuppeteerServerSetup'; 3 | import {ElementHandle} from "puppeteer"; 4 | 5 | describe('Core functionality', () => { 6 | 7 | it('should have a TouchSpin button', async () => { 8 | const selector: string = '#testinput_default'; 9 | 10 | const button = await page.$(selector + ' + .input-group-btn > .bootstrap-touchspin-up'); 11 | 12 | expect(button).toBeTruthy(); 13 | }); 14 | 15 | it('should increase value by 1 when clicking the + button', async () => { 16 | const selector: string = '#testinput_default'; 17 | 18 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 19 | await touchspinHelpers.touchspinClickUp(page, selector); 20 | 21 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('51'); 22 | }); 23 | 24 | it('should not increase value of a disabled input', async () => { 25 | const selector: string = '#testinput_default'; 26 | 27 | await touchspinHelpers.setInputAttr(page, selector, 'disabled', true); 28 | 29 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 30 | await touchspinHelpers.touchspinClickUp(page, selector); 31 | await page.keyboard.press('ArrowUp'); 32 | await page.mouse.wheel({ deltaY: -100 }); 33 | 34 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('50'); 35 | }); 36 | 37 | it('should not increase value of a readonly input', async () => { 38 | const selector: string = '#testinput_default'; 39 | 40 | await touchspinHelpers.setInputAttr(page, selector, 'readonly', true); 41 | 42 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 43 | await touchspinHelpers.touchspinClickUp(page, selector); 44 | 45 | await page.click(selector); 46 | await page.keyboard.press('ArrowUp'); 47 | await page.mouse.wheel({ deltaY: -100 }); 48 | 49 | expect(await touchspinHelpers.countEvent(page, selector, 'touchspin.on.startspin')).toBe(0); 50 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('50'); 51 | }); 52 | 53 | it('setting the input disabled should disable the touchspin buttons', async () => { 54 | const selector: string = '#testinput_default'; 55 | 56 | await touchspinHelpers.setInputAttr(page, selector, 'disabled', true); 57 | 58 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 59 | await touchspinHelpers.touchspinClickUp(page, selector); 60 | 61 | expect(await touchspinHelpers.checkTouchspinUpIsDisabled(page, selector)).toBe(true); 62 | }); 63 | 64 | it('setting the input readonly should disable the touchspin buttons', async () => { 65 | const selector: string = '#testinput_default'; 66 | 67 | await touchspinHelpers.setInputAttr(page, selector, 'readonly', true); 68 | 69 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 70 | await touchspinHelpers.touchspinClickUp(page, selector); 71 | 72 | expect(await touchspinHelpers.checkTouchspinUpIsDisabled(page, selector)).toBe(true); 73 | }); 74 | 75 | it('disabled input should initialize with disabled touchspin buttons', async () => { 76 | const selector: string = '#testinput_disabled'; 77 | 78 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 79 | await touchspinHelpers.touchspinClickUp(page, selector); 80 | 81 | expect(await touchspinHelpers.checkTouchspinUpIsDisabled(page, selector)).toBe(true); 82 | }); 83 | 84 | it('readonly input should initialize with disabled touchspin buttons', async () => { 85 | const selector: string = '#testinput_readonly'; 86 | 87 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 88 | await touchspinHelpers.touchspinClickUp(page, selector); 89 | 90 | expect(await touchspinHelpers.checkTouchspinUpIsDisabled(page, selector)).toBe(true); 91 | }); 92 | 93 | it('clicking on an input with step=3 should increase the value by 3', async () => { 94 | const selector: string = '#testinput_individual_min_max_step_properties'; 95 | 96 | // The initial value of 50 should be corrected to 51 by the browser as step = 3 97 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('51'); 98 | 99 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 100 | await touchspinHelpers.touchspinClickUp(page, selector); 101 | 102 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('54'); 103 | 104 | await touchspinHelpers.touchspinClickUp(page, selector); 105 | await touchspinHelpers.touchspinClickUp(page, selector); 106 | await touchspinHelpers.touchspinClickUp(page, selector); 107 | 108 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('57'); 109 | 110 | // Reaching 57 should fire the touchspin.on.max event three times (min is getting corrected to 45) 111 | expect(await touchspinHelpers.countEvent(page, selector, 'touchspin.on.max')).toBe(3); 112 | }); 113 | 114 | test("Bootstrap 3 should have input-group-sm class", async () => { 115 | await page.goto(`http://localhost:${port}/__tests__/html/index-bs3.html`); 116 | const selector: string = "#input_group_sm"; 117 | const inputGroupClasses = await page.$eval(selector, (input: Element) => { 118 | const inputElement = input as HTMLInputElement; 119 | return inputElement.parentElement?.className; 120 | }); 121 | expect(inputGroupClasses).toContain("input-group-sm"); 122 | }); 123 | 124 | test("Bootstrap 4 should have input-group-sm class", async () => { 125 | const selector: string = "#input_group_sm"; 126 | const inputGroupClasses = await page.$eval(selector, (input: Element) => { 127 | const inputElement = input as HTMLInputElement; 128 | return inputElement.parentElement?.className; 129 | }); 130 | expect(inputGroupClasses).toContain("input-group-sm"); 131 | }); 132 | 133 | test("Bootstrap 3 should have input-group-lg class", async () => { 134 | await page.goto(`http://localhost:${port}/__tests__/html/index-bs3.html`); 135 | const selector: string = "#input_group_lg"; 136 | const inputGroupClasses = await page.$eval(selector, (input: Element) => { 137 | const inputElement = input as HTMLInputElement; 138 | return inputElement.parentElement?.className; 139 | }); 140 | expect(inputGroupClasses).toContain("input-group-lg"); 141 | }); 142 | 143 | test("Bootstrap 4 should have input-group-lg class", async () => { 144 | const selector: string = "#input_group_lg"; 145 | const inputGroupClasses = await page.$eval(selector, (input: Element) => { 146 | const inputElement = input as HTMLInputElement; 147 | return inputElement.parentElement?.className; 148 | }); 149 | expect(inputGroupClasses).toContain("input-group-lg"); 150 | }); 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /__tests__/events.test.ts: -------------------------------------------------------------------------------- 1 | import touchspinHelpers from './helpers/touchspinHelpers'; 2 | import {page} from './helpers/jestPuppeteerServerSetup'; 3 | 4 | describe('Events', () => { 5 | 6 | it('should increase value by 1 when clicking the + button', async () => { 7 | const selector: string = '#testinput_default'; 8 | 9 | // We have to use the mousedown and mouseup events because the plugin is not handling the click event. 10 | await touchspinHelpers.touchspinClickUp(page, selector); 11 | 12 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('51'); 13 | }); 14 | 15 | it('should fire the change event only once when updating the value', async () => { 16 | const selector: string = '#testinput_default'; 17 | 18 | // Trigger the TouchSpin button 19 | await touchspinHelpers.touchspinClickUp(page, selector); 20 | 21 | // Wait for a period to ensure all events are processed (the click event is waiting for 200ms, so we are using a larger value to be on the safe side) 22 | await touchspinHelpers.waitForTimeout(300); 23 | 24 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(1); 25 | }); 26 | 27 | it('should fire the change event exactly once when entering a proper value and pressing TAB', async () => { 28 | const selector: string = '#testinput_default'; 29 | 30 | await touchspinHelpers.fillWithValue(page, selector, '67'); 31 | 32 | // Press the TAB key to move out of the input field 33 | await page.keyboard.press('Tab'); 34 | 35 | // Wait for a short period to ensure all events are processed 36 | await touchspinHelpers.waitForTimeout(500); 37 | 38 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(1); 39 | }); 40 | 41 | it('Should fire the change event only once when correcting the value according to step after pressing TAB', async () => { 42 | const selector: string = '#testinput_step10_min'; 43 | 44 | await touchspinHelpers.fillWithValue(page, selector, '67'); 45 | 46 | // Press the TAB key to move out of the input field 47 | await page.keyboard.press('Tab'); 48 | 49 | // Wait for a short period to ensure all events are processed 50 | await touchspinHelpers.waitForTimeout(500); 51 | 52 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(1); 53 | }); 54 | 55 | it('Should fire the change event only once when correcting the value according to step after pressing Enter', async () => { 56 | const selector: string = '#testinput_step10_min'; 57 | 58 | await touchspinHelpers.fillWithValue(page, selector, '67'); 59 | 60 | // Press the TAB key to move out of the input field 61 | await page.keyboard.press('Enter'); 62 | 63 | // Wait for a short period to ensure all events are processed 64 | await touchspinHelpers.waitForTimeout(500); 65 | 66 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(1); 67 | }); 68 | 69 | it('Should not fire change event when already at max value and entering a higher value', async () => { 70 | const selector: string = '#testinput_step10_max'; 71 | 72 | await touchspinHelpers.fillWithValue(page, selector, '117'); 73 | 74 | // Press the TAB key to move out of the input field 75 | await page.keyboard.press('Enter'); 76 | 77 | // Wait for a short period to ensure all events are processed 78 | await touchspinHelpers.waitForTimeout(500); 79 | 80 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(0); 81 | expect(await touchspinHelpers.countChangeWithValue(page, "100")).toBe(0); 82 | }); 83 | 84 | it('Should not fire change event when already at min value and entering a lower value', async () => { 85 | const selector: string = '#testinput_step10_min'; 86 | 87 | await touchspinHelpers.fillWithValue(page, selector, '-55'); 88 | 89 | // Press the TAB key to move out of the input field 90 | await page.keyboard.press('Enter'); 91 | 92 | // Wait for a short period to ensure all events are processed 93 | await touchspinHelpers.waitForTimeout(500); 94 | 95 | expect(await touchspinHelpers.changeEventCounter(page)).toBe(0); 96 | expect(await touchspinHelpers.countChangeWithValue(page, "0")).toBe(0); 97 | }); 98 | 99 | it('Should use the callback on the initial value', async () => { 100 | const selector: string = '#input_callbacks'; 101 | 102 | expect(await touchspinHelpers.readInputValue(page, selector)).toBe('$5,000.00'); 103 | }); 104 | 105 | it('Should have the decorated value when firing the change event', async () => { 106 | const selector: string = '#input_callbacks'; 107 | 108 | await touchspinHelpers.fillWithValue(page, selector, '1000'); 109 | 110 | await page.keyboard.press('Enter'); 111 | 112 | await touchspinHelpers.waitForTimeout(500); 113 | 114 | expect(await touchspinHelpers.countChangeWithValue(page, '$1,000.00')).toBe(1); 115 | }); 116 | 117 | it('Should have the decorated value on blur', async () => { 118 | const selector: string = '#input_callbacks'; 119 | 120 | await touchspinHelpers.fillWithValue(page, selector, '1000'); 121 | 122 | await page.click('#input_group_lg', { clickCount: 1 }); 123 | 124 | expect(await touchspinHelpers.countChangeWithValue(page, '1000')).toBe(0); 125 | expect(await touchspinHelpers.countChangeWithValue(page, '$1,000.00')).toBe(1); 126 | }); 127 | 128 | it('The touchspin.on.min and touchspin.on.max events should fire as soon as the value reaches the minimum or maximum value', async () => { 129 | const selector: string = '#testinput_default'; 130 | 131 | await touchspinHelpers.fillWithValue(page, selector, '1'); 132 | await page.keyboard.press('ArrowDown'); 133 | expect(await touchspinHelpers.countEvent(page, selector, 'touchspin.on.min')).toBe(1); 134 | 135 | await touchspinHelpers.fillWithValue(page, selector, '99'); 136 | await page.keyboard.press('ArrowUp'); 137 | expect(await touchspinHelpers.countEvent(page, selector, 'touchspin.on.max')).toBe(1); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /__tests__/helpers/jestPuppeteerServerSetup.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import puppeteer, {Browser, Page} from "puppeteer"; 4 | 5 | const puppeteerDebug = process.env.PUPPETEER_DEBUG === '1'; 6 | 7 | const app = express(); 8 | const port = 8080; 9 | 10 | app.use(express.static(path.join(__dirname, '../..'))); 11 | 12 | let server: any; 13 | let browser: Browser; 14 | let page: Page; 15 | 16 | beforeAll(async () => { 17 | server = app.listen(port, () => { 18 | console.log(`Express server listening on port ${port}...`); 19 | }); 20 | 21 | if (puppeteerDebug) { 22 | browser = await puppeteer.launch({ 23 | headless: false, 24 | slowMo: 300, 25 | devtools: true, 26 | }); 27 | } else { 28 | browser = await puppeteer.launch({ 29 | headless: true, 30 | }); 31 | } 32 | }); 33 | 34 | afterAll(async () => { 35 | await browser.close(); 36 | await new Promise((resolve) => server.close(resolve)); 37 | }); 38 | 39 | beforeEach(async () => { 40 | if (!page) { 41 | // Create a new page if it doesn't exist 42 | page = await browser.newPage(); 43 | } 44 | 45 | await page.goto(`http://localhost:${port}/__tests__/html/index-bs4.html`); 46 | }); 47 | 48 | export { page, port }; 49 | -------------------------------------------------------------------------------- /__tests__/helpers/touchspinHelpers.ts: -------------------------------------------------------------------------------- 1 | import {Page} from 'puppeteer'; 2 | 3 | async function waitForTimeout(ms: number): Promise { 4 | return new Promise(r => setTimeout(r, ms)); 5 | } 6 | 7 | async function readInputValue(page: Page, selector: string): Promise { 8 | const input = await page.$(selector); 9 | return await input?.evaluate((el) => (el as HTMLInputElement).value); 10 | } 11 | 12 | async function setInputAttr(page: Page, selector: string, attributeName: 'disabled' | 'readonly', attributeValue: boolean): Promise { 13 | const input = await page.$(selector); 14 | await input?.evaluate((el, attributeName, attributeValue) => { 15 | if (attributeValue) { 16 | (el as HTMLInputElement).setAttribute(attributeName, ''); 17 | } else { 18 | (el as HTMLInputElement).removeAttribute(attributeName); 19 | } 20 | }, attributeName, attributeValue); 21 | } 22 | 23 | async function checkTouchspinUpIsDisabled(page: Page, selector: string): Promise { 24 | const input = await page.$(selector + ' + .input-group-btn > .bootstrap-touchspin-up'); 25 | 26 | return await input!.evaluate((el) => { 27 | return (el as HTMLInputElement).hasAttribute('disabled'); 28 | }); 29 | } 30 | 31 | async function touchspinClickUp(page: Page, input_selector: string): Promise { 32 | await page.evaluate((selector) => { 33 | document.querySelector(selector)!.dispatchEvent(new Event('mousedown')); 34 | }, input_selector + ' + .input-group-btn > .bootstrap-touchspin-up'); 35 | 36 | // Delay to allow the value to change. 37 | await new Promise(r => setTimeout(r, 200)); 38 | 39 | await page.evaluate((selector) => { 40 | document.querySelector(selector)!.dispatchEvent(new Event('mouseup')); 41 | }, input_selector + ' + .input-group-btn > .bootstrap-touchspin-up'); 42 | } 43 | 44 | async function changeEventCounter(page: Page): Promise { 45 | // Get the event log content 46 | const eventLogContent = await page.$eval('#events_log', el => el.textContent); 47 | 48 | // Count the number of 'change' events 49 | return (eventLogContent?.match(/change\[/g) ?? []).length; 50 | } 51 | 52 | async function countChangeWithValue(page: Page, expectedValue: string): Promise { 53 | const expectedText = '#input_callbacks: change[' + expectedValue + ']'; 54 | return await page.evaluate((text) => { 55 | return Array.from(document.querySelectorAll('#events_log')) 56 | .filter(element => element.textContent!.includes(text)).length; 57 | }, expectedText); 58 | } 59 | 60 | async function countEvent(page: Page, selector: string, event: string): Promise { 61 | // Get the event log content 62 | const eventLogContent = await page.$eval('#events_log', el => el.textContent); 63 | 64 | // Count the number of 'change' events with the expected value 65 | const searchString = selector + ': ' + event; 66 | return (eventLogContent ? eventLogContent.split(searchString).length - 1 : 0); 67 | } 68 | 69 | async function fillWithValue(page: Page, selector: string, value: string): Promise { 70 | await page.focus(selector); 71 | // Has to be triple click to select all text when using decorators 72 | await page.click(selector, { clickCount: 3 }); 73 | await page.keyboard.type(value); 74 | } 75 | 76 | export default { waitForTimeout, readInputValue, setInputAttr, checkTouchspinUpIsDisabled, touchspinClickUp, changeEventCounter, countEvent, countChangeWithValue, fillWithValue }; 77 | -------------------------------------------------------------------------------- /__tests__/html/index-bs3-vertical.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 49 |
50 | 51 |
52 | 53 | 54 | 55 | 60 |
61 | 62 |
63 | 64 | 65 | 66 | 73 |
74 | 75 |
76 | 77 |
78 |
Events
79 |

 81 |     
82 | 83 | 115 | 116 |
117 | 118 |
119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /__tests__/html/index-bs3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 55 |
56 | 57 |
58 | 59 | 60 | 61 | 66 |
67 | 68 |
69 | 70 | 71 | 72 | 77 |
78 | 79 |
80 | 81 | 82 | 83 | 86 |
87 | 88 |
89 | 90 | 91 | 92 | 95 |
96 | 97 |
98 | 99 | 105 | 106 | 109 |
110 | 111 |
112 | 113 | 122 | 123 | 126 |
127 | 128 |
129 | 130 | 131 | 132 | 138 |
139 | 140 |
141 | 142 | 143 | 144 | 150 |
151 | 152 |
153 | 154 | 155 | 156 | 161 |
162 | 163 |
164 | 165 | 166 | 167 | 172 |
173 | 174 |
175 | 176 | 177 | 178 | 183 |
184 | 185 |
186 | 187 | 188 | 189 | 196 |
197 | 198 |
199 | 200 | 201 | 202 | 209 |
210 | 211 |
212 | 213 | 214 | 215 | 222 |
223 | 224 |
225 | 226 |
227 | pre 228 | 229 | post 230 |
231 | 232 | 237 |
238 | 239 |
240 | 241 |
242 | pre 243 | 244 |
245 | 246 | 251 |
252 | 253 |
254 | 255 |
256 | pre 257 | 258 | post 259 |
260 | 261 | 264 |
265 | 266 |
267 | 268 |
269 | pre 270 | 271 |
272 | 273 | 276 |
277 | 278 |
279 | 280 |
281 |
Events
282 |

284 |     
285 | 286 | 318 | 319 |
320 | 321 |
322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /__tests__/html/index-bs4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 57 |
58 | 59 |
60 | 61 | 62 | 63 | 68 |
69 | 70 |
71 | 72 | 73 | 74 | 79 |
80 | 81 |
82 | 83 | 84 | 85 | 88 |
89 | 90 |
91 | 92 | 93 | 94 | 97 |
98 | 99 |
100 | 101 | 107 | 108 | 111 |
112 | 113 |
114 | 115 | 124 | 125 | 128 |
129 | 130 |
131 | 132 | 133 | 134 | 140 |
141 | 142 |
143 | 144 | 145 | 146 | 152 |
153 | 154 |
155 | 156 | 157 | 158 | 173 |
174 | 175 |
176 | 177 | 178 | 179 | 184 |
185 | 186 |
187 | 188 | 189 | 190 | 195 |
196 | 197 |
198 | 199 | 200 | 201 | 206 |
207 | 208 |
209 | 210 | 211 | 212 | 219 |
220 | 221 |
222 | 223 | 224 | 225 | 232 |
233 | 234 |
235 | 236 | 237 | 238 | 245 |
246 | 247 |
248 | 249 |
250 |
251 | pre 252 |
253 | 254 |
255 | post 256 |
257 |
258 | 259 | 264 |
265 | 266 |
267 | 268 |
269 |
270 | pre 271 |
272 | 273 |
274 | 275 | 280 |
281 | 282 |
283 | 284 |
285 |
286 | pre 287 |
288 | 289 |
290 | post 291 |
292 |
293 | 294 | 297 |
298 | 299 |
300 | 301 |
302 |
303 | pre 304 |
305 | 306 |
307 | 308 | 311 |
312 | 313 |
314 | 315 |
316 |
Events
317 |

319 |     
320 | 321 | 353 | 354 |
355 | 356 | 357 |
358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /__tests__/html/index-vertical.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | 51 |
52 | 53 |
54 | 55 | 56 | 57 | 62 |
63 | 64 |
65 | 66 | 67 | 68 | 75 |
76 | 77 |
78 | 79 |
80 |
Events
81 |

 83 |     
84 | 85 | 117 | 118 |
119 | 120 | 121 |
122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-touchspin", 3 | "version": "4.3.0", 4 | "homepage": "http://www.virtuosoft.eu/code/bootstrap-touchspin/", 5 | "authors": [ 6 | { 7 | "name": "István Ujj-Mészáros", 8 | "url": "https://github.com/istvan-ujjmeszaros" 9 | } 10 | ], 11 | "description": "Bootstrap TouchSpin is a mobile and touch friendly input spinner component for Bootstrap 3 & 4.", 12 | "dependencies": { 13 | "jquery": ">=1.9.0", 14 | "bootstrap": ">=3.0.0" 15 | }, 16 | "license": "MIT", 17 | "main": [ 18 | "src/jquery.bootstrap-touchspin.css", 19 | "src/jquery.bootstrap-touchspin.js" 20 | ], 21 | "keywords": [ 22 | "jquery", 23 | "plugin", 24 | "bootstrap", 25 | "ui" 26 | ], 27 | "ignore": [ 28 | "**/.*", 29 | "node_modules", 30 | "bower_components", 31 | "test", 32 | "tests" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Ubuntu, Helvetica, sans-serif; 3 | color: #333; 4 | padding-top: 20px; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { 8 | font-family: Arial, Ubuntu, Helvetica, sans-serif; 9 | font-weight: normal; 10 | } 11 | 12 | hr { 13 | border-color: #ccc; 14 | } 15 | 16 | a, 17 | a:hover { 18 | color: #d64513; 19 | } 20 | 21 | p { 22 | margin-bottom: 15px; 23 | } 24 | 25 | h1 { 26 | font-family: "Century Gothic", Arial, Ubuntu, Helvetica, sans-serif; 27 | font-size: 28px; 28 | } 29 | 30 | h1 small { 31 | font-size: 22px; 32 | } 33 | 34 | h2 { 35 | font-family: "Century Gothic", Arial, Ubuntu, Helvetica, sans-serif; 36 | font-size: 26px; 37 | margin: 30px 0 20px; 38 | } 39 | 40 | h2 small { 41 | font-size: 18px; 42 | } 43 | 44 | h3 { 45 | margin-top: 25px; 46 | font-size: 18px; 47 | font-weight: bold; 48 | } 49 | 50 | h3 small { 51 | font-size: 16px; 52 | } 53 | 54 | h4 { 55 | font-size: 18px; 56 | } 57 | 58 | h4 small { 59 | font-size: 14px; 60 | } 61 | 62 | h5 { 63 | font-size: 16px; 64 | } 65 | 66 | h5 small { 67 | font-size: 12px; 68 | } 69 | 70 | h6 { 71 | font-size: 14px; 72 | } 73 | 74 | h6 small { 75 | font-size: 10px; 76 | } 77 | 78 | .dl-horizontal dt { 79 | width: 200px; 80 | } 81 | 82 | .dl-horizontal dd { 83 | margin-left: 220px; 84 | } 85 | 86 | small { 87 | font-size: 12px; 88 | } 89 | 90 | .panel { 91 | border-radius: 10px; 92 | background: #f5f5f5; 93 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 94 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 95 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 96 | } 97 | 98 | .panel-heading { 99 | border-bottom: 1px solid #ddd; 100 | color: #4d4d4d; 101 | font-size: 14px; 102 | margin: 0; 103 | } 104 | 105 | .panel-heading a { 106 | color: #4d4d4d; 107 | } 108 | 109 | .panel-body { 110 | border-top: 1px solid #fff; 111 | } 112 | 113 | .abstract { 114 | min-height: 60px; 115 | } 116 | 117 | .btn-info { 118 | border: none; 119 | background: #d64513; 120 | color: #fff; 121 | box-shadow: none; 122 | text-shadow: none; 123 | } 124 | 125 | .btn-info:hover, .btn-info:focus, .btn-info:active, .btn-info.active, .btn-info.disabled, .btn-info[disabled] { 126 | background: #a5360f; 127 | } 128 | 129 | /* woodpaul on board */ 130 | .hero-unit { 131 | text-align: center; 132 | margin: 0 0 50px; 133 | } 134 | 135 | .hero-unit h1 { 136 | font-size: 48px; 137 | text-align: center; 138 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 1); 139 | } 140 | 141 | .hero-unit h1 small { 142 | display: block; 143 | } 144 | 145 | .hero-unit .btn { 146 | margin: 0; 147 | } 148 | 149 | hr { 150 | background-color: transparent; 151 | border-top: 1px solid #ddd; 152 | border-bottom: 1px solid #fff; 153 | } 154 | 155 | .controls-row { 156 | margin: 0 0 10px; 157 | } 158 | 159 | /* table */ 160 | .table thead th { 161 | font-family: "Century Gothic", Arial, sans-serif; 162 | text-transform: uppercase; 163 | font-weight: normal; 164 | background-color: #bc451b; 165 | color: #fff; 166 | vertical-align: middle !important; 167 | } 168 | 169 | code { 170 | padding: 1px 4px; 171 | } 172 | 173 | -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istvan-ujjmeszaros/bootstrap-touchspin/8b7db229c5838508f8c1960efb0a4d321fddc058/demo/favicon.ico -------------------------------------------------------------------------------- /demo/index-bs3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |

Bootstrap TouchSpin

39 | 40 | 41 | Github project page 42 | 43 | Download 44 | 45 |
46 | 47 |

48 | A mobile and touch friendly input spinner component for Bootstrap 3 & 4.
49 | It supports the mousewheel and the up/down keys.

50 | 51 |

CSS file is necessary only if you are using vertical buttons functionality. In other cases plugin is using boostrap 52 | input-group component.

53 | 54 |

Examples

55 | 56 |
57 |
58 | 59 | 60 |
61 | 62 |
63 |
 64 | <input id="demo0"
 65 |        type="text"
 66 |        value="55"
 67 |        name="demo0"
 68 |        data-bts-min="0"
 69 |        data-bts-max="100"
 70 |        data-bts-init-val=""
 71 |        data-bts-step="1"
 72 |        data-bts-decimal="0"
 73 |        data-bts-step-interval="100"
 74 |        data-bts-force-step-divisibility="round"
 75 |        data-bts-step-interval-delay="500"
 76 |        data-bts-prefix=""
 77 |        data-bts-postfix=""
 78 |        data-bts-prefix-extra-class=""
 79 |        data-bts-postfix-extra-class=""
 80 |        data-bts-booster="true"
 81 |        data-bts-boostat="10"
 82 |        data-bts-max-boosted-step="false"
 83 |        data-bts-mousewheel="true"
 84 |        data-bts-button-down-class="btn btn-secondary"
 85 |        data-bts-button-up-class="btn btn-secondary"
 86 |         />
 87 | <script>
 88 |     $("input[name='demo0']").TouchSpin({
 89 |     });
 90 | </script>
 91 | 
92 | 93 | 96 | 97 |
98 |
99 | 100 |
101 |
102 | 104 | 105 |
106 | 107 |
108 |
109 | <input id="demo_callback" type="text"
110 |        value="10000.00"
111 |        name="demo_callback"
112 |        data-bts-min="-1000000"
113 |        data-bts-max="1000000"
114 |        data-bts-step=".01"
115 |        data-bts-decimals="2"
116 | />
117 | <script src="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js"></script>
118 | <script>
119 |   $("input[name='demo_callback']").TouchSpin({
120 |     callback_before_calculation: function(v){
121 |       return numeral(v).value();
122 |     },
123 |     callback_after_calculation: function(v){
124 |       return numeral(v).format("$0,0.00");
125 |     }
126 |   });
127 | </script>
128 | 
129 | 130 | 140 | 141 |
142 |
143 | 144 | 145 |
146 |
147 | 148 | 149 |
150 | 151 |
152 |
153 | <input id="demo_vertical" type="text" value="" name="demo_vertical">
154 | <script>
155 |     $("input[name='demo_vertical']").TouchSpin({
156 |       verticalbuttons: true
157 |     });
158 | </script>
159 | 
160 | 161 | 166 | 167 |
168 |
169 | 170 |
171 |
172 | 173 | 174 |
175 | 176 |
177 |
178 | <input id="demo_vertical2" type="text" value="" name="demo_vertical2">
179 | <script>
180 |     $("input[name='demo_vertical2']").TouchSpin({
181 |       verticalbuttons: true,
182 |       verticalup: '<i class="fas fa-caret-up"></i>',
183 |       verticaldown: '<i class="fas fa-caret-down"></i>'
184 |     });
185 | </script>
186 | 
187 | 188 | 195 | 196 |
197 |
198 | 199 |
200 |
201 | 202 |
203 | 204 |
205 |
206 | <input id="demo1" type="text" value="55" name="demo1">
207 | <script>
208 |     $("input[name='demo1']").TouchSpin({
209 |         min: 0,
210 |         max: 100,
211 |         step: 0.1,
212 |         decimals: 2,
213 |         boostat: 5,
214 |         maxboostedstep: 10,
215 |         postfix: '%'
216 |     });
217 | </script>
218 | 
219 | 220 | 231 | 232 |
233 |
234 |
235 |
236 |
237 |
238 | 239 | 240 |
241 |
242 |
243 | 244 |
245 |
246 | <form class="form-horizontal" role="form">
247 |     <div class="form-group">
248 |         <label for="demo2" class="col-md-5 control-label">Example:</label> <input id="demo2" type="text" value="0" name="demo2" class="col-md-7 form-control">
249 |     </div>
250 | </form>
251 | 
252 | <script>
253 |     $("input[name='demo2']").TouchSpin({
254 |         min: -1000000000,
255 |         max: 1000000000,
256 |         stepinterval: 50,
257 |         maxboostedstep: 10000000,
258 |         prefix: '$'
259 |     });
260 | </script>
261 | 
262 | 271 | 272 |
273 |
274 | 275 |
276 |
277 | 278 |
279 | 280 |
281 |
282 | <input id="demo3" type="text" value="" name="demo3">
283 | <script>
284 |     $("input[name='demo3']").TouchSpin();
285 | </script>
286 | 
287 | 288 | 291 | 292 |
293 |
294 | 295 |
296 |

297 | The initval setting is only applied when no explicit value is set on the input with the 298 | value attribute.

299 | 300 |
301 | 304 | 307 |
308 | 309 |
310 |
311 | <input id="demo3_21" type="text" class="input-lg" value="" name="demo3_21">
312 | <script>
313 |     $("input[name='demo3_21']").TouchSpin({
314 |         initval: 40
315 |     });
316 | </script>
317 | <input id="demo3_22" type="text" class="input-sm" value="33" name="demo3_22">
318 | <script>
319 |     $("input[name='demo3_22']").TouchSpin({
320 |         initval: 40
321 |     });
322 | </script>
323 | 
324 | 325 | 333 | 334 |
335 |
336 | 337 |

338 | Size of the whole controller can be set with applying input-sm or input-lg class on the 339 | input, or by applying the plugin on an input inside an input-group with the proper size class(input-group-sm 340 | or input-group-lg).

341 | 342 |
343 |
344 | 345 | 346 |
347 | 348 |
349 |
350 | <input id="demo4" type="text" value="" name="demo4" class="input-sm">
351 | <script>
352 |     $("input[name='demo4']").TouchSpin({
353 |         postfix: "a button",
354 |         postfix_extraclass: "btn btn-primary"
355 |     });
356 | </script>
357 | 
358 | 359 | 365 | 366 |
367 |
368 | 369 |
370 |
371 | 372 | 373 |
374 | 375 |
376 |
377 | 378 |
379 |
380 | <div class="input-group input-group-lg">
381 |     <input id="demo4_2" type="text" value="" name="demo4_2" class="form-control input-lg">
382 | </div>
383 | <script>
384 |     $("input[name='demo4_2']").TouchSpin({
385 |         postfix: "a button",
386 |         postfix_extraclass: "btn btn-primary"
387 |     });
388 | </script>
389 | 
390 | 391 | 397 | 398 |
399 |
400 | 401 |
402 |
403 | 404 | 405 | 406 | little different for bootstrap v4 407 | 408 | 409 |
410 | 411 | 412 |
413 | 414 | 418 | 425 |
426 |
427 | 428 |
429 | 430 |
431 |
432 | <div class="input-group">
433 |     <input id="demo5" type="text" class="form-control" name="demo5" value="50">
434 |     <div class="input-group-btn">
435 |         <button type="button" class="btn btn-default">Action</button>
436 |         <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
437 |             <span class="caret"></span>
438 |             <span class="sr-only">Toggle Dropdown</span>
439 |         </button>
440 |         <ul class="dropdown-menu pull-right" role="menu">
441 |             <li><a href="#">Action</a></li>
442 |             <li><a href="#">Another action</a></li>
443 |             <li><a href="#">Something else here</a></li>
444 |             <li class="divider"></li>
445 |             <li><a href="#">Separated link</a></li>
446 |         </ul>
447 |     </div>
448 | </div>
449 | <script>
450 |     $("input[name='demo5']").TouchSpin({
451 |         prefix: "pre",
452 |         postfix: "post"
453 |     });
454 | </script>
455 | 
456 | 457 | 463 | 464 |
465 |
466 | 467 |
468 |
469 | 470 |
471 | 472 |
473 |
474 | $("input[name='demo6']").TouchSpin({
475 |     buttondown_class: "btn btn-link",
476 |     buttonup_class: "btn btn-link"
477 | });
478 | 
479 | 480 | 486 | 487 |
488 |
489 | 490 |
491 |
492 | 493 |
494 | 495 |
496 |
497 | $("input[name='demo7']").TouchSpin({
498 |     replacementval: 10
499 | });
500 | 
501 | 502 | 507 | 508 |
509 |
510 | 511 |

By setting the min or max option to NULL no limit is enfored to the particular boundary.

512 | 513 |
514 |
515 | 516 |
517 | 518 |
519 |
520 | <input id="demo8-1" type="text" value="1234567890" name="demo8-1">
521 | <script>
522 |     $("input[name='demo8-1']").TouchSpin({
523 |         min: null,
524 |         max: null
525 |     });
526 | </script>
527 | 
528 | 529 | 535 | 536 |
537 |
538 | 539 |
540 |
541 | 542 | 543 |
544 |
545 |

546 |     
547 | 548 | 587 | 588 |
589 | 590 |

Settings

591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 606 | 607 | 608 | 609 | 610 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 |
OptionDefaultDescription
initval""Applied when no explicit value is set on the input with the value attribute. Empty string means 604 | that the value remains empty on initialization. 605 |
replacementval""Applied when user leaves the field empty/blank or enters non-number. Empty string means that the value will 611 | not be replaced. 612 |
min0Minimum value. Can be null, when there is no boundary check.
max100Maximum value. Can be null, when there is no boundary check.
step1Incremental/decremental step on up/down change.
forcestepdivisibility'round'How to force the value to be divisible by step value: 'none' | 'round' | 'floor' 633 | | 'ceil'
decimals0Number of decimal points.
stepinterval100Refresh rate of the spinner in milliseconds.
stepintervaldelay500Time in milliseconds before the spinner starts to spin.
verticalbuttonsfalseEnables the traditional up/down buttons.
verticalup+Content of the up button with vertical buttons mode enabled.
verticaldown-Content of the down button with vertical buttons mode enabled.
verticalupclass""Class of the up button with vertical buttons mode enabled.
verticaldownclass""Class of the down button with vertical buttons mode enabled.
prefix""Text before the input.
postfix""Text after the input.
prefix_extraclass""Extra class(es) for prefix.
postfix_extraclass""Extra class(es) for postfix.
boostertrueIf enabled, the the spinner is continually becoming faster as holding the button.
boostat10Boost at every nth step.
maxboostedstepfalseMaximum step when boosted.
mousewheeltrueEnables the mouse wheel to change the value of the input.
buttondown_class'btn btn-primary'Class(es) of down button.
buttonup_class'btn btn-primary'Class(es) of up button.
buttondown_txt'-'Content inside the down button.
buttonup_txt'+'Content inside the up button.
737 | 738 |

Events

739 | 740 |

Triggered events

741 | 742 |

The following events are triggered on the original input by the plugin and can be listened on.

743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 |
EventDescription
changeTriggered when the value is changed with one of the buttons (but not triggered when the spinner hits the 755 | limit set by settings.min or settings.max. 756 |
touchspin.on.startspinTriggered when the spinner starts spinning upwards or downwards.
touchspin.on.startupspinTriggered when the spinner starts spinning upwards.
touchspin.on.startdownspinTriggered when the spinner starts spinning downwards.
touchspin.on.stopspinTriggered when the spinner stops spinning.
touchspin.on.stopupspinTriggered when the spinner stops upspinning.
touchspin.on.stopdownspinTriggered when the spinner stops downspinning.
touchspin.on.minTriggered when the spinner hits the limit set by settings.min.
touchspin.on.maxTriggered when the spinner hits the limit set by settings.max.
792 | 793 |

Callable events

794 | 795 |

The following events can be triggered on the original input.

796 | 797 |

798 | Example usages:
799 | $("input").trigger("touchspin.uponce");
800 | $("input").trigger("touchspin.updatesettings", {max: 1000}); 801 |

802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 |
EventDescription
touchspin.updatesettingsfunction(newoptions): Update any setting of an already initialized TouchSpin instance.
touchspin.uponceIncrease the value by one step.
touchspin.downonceDecrease the value by one step.
touchspin.startupspinStarts the spinner upwards.
touchspin.startdownspinStarts the spinner downwards.
touchspin.stopspinStops the spinner.
837 | 838 |

Download

839 | 840 |

Download from 841 | github. Please report issues and suggestions to github's issue tracker or contact me on 842 | g+ or 843 | twitter!

844 | 845 |
846 | 847 | 850 | 851 | 852 | 853 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bootstrap TouchSpin 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |

Bootstrap TouchSpin

39 | 40 | 41 | Github project page 42 | 43 | Download 44 | 45 |
46 | 47 |

48 | A mobile and touch friendly input spinner component for Bootstrap 3 & 4.
49 | It supports the mousewheel and the up/down keys.

50 | 51 |

CSS file is necessary only if you are using vertical buttons functionality. In other cases plugin is using boostrap 52 | input-group component.

53 | 54 |

Examples

55 | 56 |
57 |
58 | 59 | 60 |
61 | 62 |
63 |
 64 | <input id="demo0"
 65 |        type="text"
 66 |        value="55"
 67 |        name="demo0"
 68 |        data-bts-min="0"
 69 |        data-bts-max="100"
 70 |        data-bts-init-val=""
 71 |        data-bts-step="1"
 72 |        data-bts-decimal="0"
 73 |        data-bts-step-interval="100"
 74 |        data-bts-force-step-divisibility="round"
 75 |        data-bts-step-interval-delay="500"
 76 |        data-bts-prefix=""
 77 |        data-bts-postfix=""
 78 |        data-bts-prefix-extra-class=""
 79 |        data-bts-postfix-extra-class=""
 80 |        data-bts-booster="true"
 81 |        data-bts-boostat="10"
 82 |        data-bts-max-boosted-step="false"
 83 |        data-bts-mousewheel="true"
 84 |        data-bts-button-down-class="btn btn-secondary"
 85 |        data-bts-button-up-class="btn btn-secondary"
 86 |         />
 87 | <script>
 88 |     $("input[name='demo0']").TouchSpin();
 89 | </script>
 90 | 
91 | 92 | 95 | 96 |
97 |
98 | 99 |
100 |
101 | 103 | 104 |
105 | 106 |
107 |
108 | <input id="demo_callback" type="text"
109 |        value="10000.00"
110 |        name="demo_callback"
111 |        data-bts-min="-1000000"
112 |        data-bts-max="1000000"
113 |        data-bts-step=".01"
114 |        data-bts-decimals="2"
115 | />
116 | <script src="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js"></script>
117 | <script>
118 |   $("input[name='demo_callback']").TouchSpin({
119 |     callback_before_calculation: function(v){
120 |       return numeral(v).value();
121 |     },
122 |     callback_after_calculation: function(v){
123 |       return numeral(v).format("$0,0.00");
124 |     }
125 |   });
126 | </script>
127 | 
128 | 129 | 139 | 140 |
141 |
142 | 143 |
144 |
145 | 146 | 147 |
148 | 149 |
150 |
151 | <input id="demo_vertical" type="text" value="" name="demo_vertical">
152 | <script>
153 |     $("input[name='demo_vertical']").TouchSpin({
154 |       verticalbuttons: true
155 |     });
156 | </script>
157 | 
158 | 159 | 164 | 165 |
166 |
167 | 168 |
169 |
170 | 171 | 172 |
173 | 174 |
175 |
176 | <input id="demo_vertical2" type="text" value="" name="demo_vertical2">
177 | <script>
178 |     $("input[name='demo_vertical2']").TouchSpin({
179 |       verticalbuttons: true,
180 |       verticalup: '<i class="fas fa-caret-up"></i>',
181 |       verticaldown: '<i class="fas fa-caret-down"></i>'
182 |     });
183 | </script>
184 | 
185 | 186 | 193 | 194 |
195 |
196 | 197 |
198 |
199 | 200 |
201 | 202 |
203 |
204 | <input id="demo1" type="text" value="55" name="demo1">
205 | <script>
206 |     $("input[name='demo1']").TouchSpin({
207 |         min: 0,
208 |         max: 100,
209 |         step: 0.1,
210 |         decimals: 2,
211 |         boostat: 5,
212 |         maxboostedstep: 10,
213 |         postfix: '%'
214 |     });
215 | </script>
216 | 
217 | 218 | 229 | 230 |
231 |
232 |
233 |
234 |
235 |
236 | 237 | 238 |
239 |
240 |
241 | 242 |
243 |
244 | <form class="form-horizontal" role="form">
245 |     <div class="form-group">
246 |         <label for="demo2" class="col-md-5 control-label">Example:</label> <input id="demo2" type="text" value="0" name="demo2" class="col-md-7 form-control">
247 |     </div>
248 | </form>
249 | 
250 | <script>
251 |     $("input[name='demo2']").TouchSpin({
252 |         min: -1000000000,
253 |         max: 1000000000,
254 |         stepinterval: 50,
255 |         maxboostedstep: 10000000,
256 |         prefix: '$'
257 |     });
258 | </script>
259 | 
260 | 269 | 270 |
271 |
272 | 273 |
274 |
275 | 276 |
277 | 278 |
279 |
280 | <input id="demo3" type="text" value="" name="demo3">
281 | <script>
282 |     $("input[name='demo3']").TouchSpin();
283 | </script>
284 | 
285 | 286 | 289 | 290 |
291 |
292 | 293 |
294 |

295 | The initval setting is only applied when no explicit value is set on the input with the 296 | value attribute.

297 | 298 |
299 | 302 | 305 |
306 | 307 |
308 |
309 | <input id="demo3_21" type="text" class="input-lg" value="" name="demo3_21">
310 | <script>
311 |     $("input[name='demo3_21']").TouchSpin({
312 |         initval: 40
313 |     });
314 | </script>
315 | <input id="demo3_22" type="text" class="input-sm" value="33" name="demo3_22">
316 | <script>
317 |     $("input[name='demo3_22']").TouchSpin({
318 |         initval: 40
319 |     });
320 | </script>
321 | 
322 | 323 | 331 | 332 |
333 |
334 | 335 |

336 | Size of the whole controller can be set with applying input-sm or input-lg class on the 337 | input, or by applying the plugin on an input inside an input-group with the proper size class(input-group-sm 338 | or input-group-lg).

339 | 340 |
341 |
342 | 343 | 344 |
345 | 346 |
347 |
348 | <input id="demo4" type="text" value="" name="demo4" class="input-sm">
349 | <script>
350 |     $("input[name='demo4']").TouchSpin({
351 |         postfix: "a button",
352 |         postfix_extraclass: "btn btn-primary"
353 |     });
354 | </script>
355 | 
356 | 357 | 363 | 364 |
365 |
366 | 367 |
368 |
369 | 370 | 371 |
372 | 373 |
374 |
375 | 376 |
377 |
378 | <div class="input-group input-group-lg">
379 |     <input id="demo4_2" type="text" value="" name="demo4_2" class="form-control input-lg">
380 | </div>
381 | <script>
382 |     $("input[name='demo4_2']").TouchSpin({
383 |         postfix: "a button",
384 |         postfix_extraclass: "btn btn-primary"
385 |     });
386 | </script>
387 | 
388 | 389 | 395 | 396 |
397 |
398 | 399 |
400 |
401 | 402 | 403 |
404 | 405 | 406 |
407 | 408 | 409 | 416 |
417 |
418 | 419 |
420 | 421 |
422 |
423 | <div class="input-group">
424 |     <input id="demo5" type="text" class="form-control" name="demo5" value="50">
425 | 
426 |     <div class="input-group-append">
427 |         <button type="button" class="btn btn-success">Action</button>
428 |         <button class="btn btn-info dropdown-toggle" type="button" data-toggle="dropdown">Dropdown</button>
429 |         <div class="dropdown-menu">
430 |             <a class="dropdown-item" href="#">Action</a>
431 |             <a class="dropdown-item" href="#">Another action</a>
432 |             <a class="dropdown-item" href="#">Something else here</a>
433 |             <div role="separator" class="dropdown-divider"></div>
434 |             <a class="dropdown-item" href="#">Separated link</a>
435 |         </div>
436 |     </div>
437 | </div>
438 | <script>
439 |     $("input[name='demo5']").TouchSpin({
440 |         prefix: "pre",
441 |         postfix: "post"
442 |     });
443 | </script>
444 | 
445 | 446 | 452 | 453 |
454 |
455 | 456 |
457 |
458 | 459 |
460 | 461 |
462 |
463 | $("input[name='demo6']").TouchSpin({
464 |     buttondown_class: "btn btn-link",
465 |     buttonup_class: "btn btn-link"
466 | });
467 | 
468 | 469 | 475 | 476 |
477 |
478 | 479 |
480 |
481 | 482 |
483 | 484 |
485 |
486 | $("input[name='demo7']").TouchSpin({
487 |     replacementval: 10
488 | });
489 | 
490 | 491 | 496 | 497 |
498 |
499 | 500 |

By setting the min or max option to NULL no limit is enfored to the particular boundary.

501 | 502 |
503 |
504 | 505 |
506 | 507 |
508 |
509 | <input id="demo8-1" type="text" value="1234567890" name="demo8-1">
510 | <script>
511 |     $("input[name='demo8-1']").TouchSpin({
512 |         min: null,
513 |         max: null
514 |     });
515 | </script>
516 | 
517 | 518 | 524 | 525 |
526 |
527 | 528 |
529 |
530 | 531 | 532 |
533 |
534 |

535 |     
536 | 537 | 576 | 577 |
578 | 579 |

Settings

580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 595 | 596 | 597 | 598 | 599 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 |
OptionDefaultDescription
initval""Applied when no explicit value is set on the input with the value attribute. Empty string means 593 | that the value remains empty on initialization. 594 |
replacementval""Applied when user leaves the field empty/blank or enters non-number. Empty string means that the value will 600 | not be replaced. 601 |
min0Minimum value. Can be null, when there is no boundary check.
max100Maximum value. Can be null, when there is no boundary check.
step1Incremental/decremental step on up/down change.
forcestepdivisibility'round'How to force the value to be divisible by step value: 'none' | 'round' | 'floor' 622 | | 'ceil'
decimals0Number of decimal points.
stepinterval100Refresh rate of the spinner in milliseconds.
stepintervaldelay500Time in milliseconds before the spinner starts to spin.
verticalbuttonsfalseEnables the traditional up/down buttons.
verticalup+Content of the up button with vertical buttons mode enabled.
verticaldown-Content of the down button with vertical buttons mode enabled.
verticalupclass""Class of the up button with vertical buttons mode enabled.
verticaldownclass""Class of the down button with vertical buttons mode enabled.
prefix""Text before the input.
postfix""Text after the input.
prefix_extraclass""Extra class(es) for prefix.
postfix_extraclass""Extra class(es) for postfix.
boostertrueIf enabled, the the spinner is continually becoming faster as holding the button.
boostat10Boost at every nth step.
maxboostedstepfalseMaximum step when boosted.
mousewheeltrueEnables the mouse wheel to change the value of the input.
buttondown_class'btn btn-primary'Class(es) of down button.
buttonup_class'btn btn-primary'Class(es) of up button.
buttondown_txt'-'Content inside the down button.
buttonup_txt'+'Content inside the up button.
726 | 727 |

Events

728 | 729 |

Triggered events

730 | 731 |

The following events are triggered on the original input by the plugin and can be listened on.

732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 |
EventDescription
changeTriggered when the value is changed with one of the buttons (but not triggered when the spinner hits the 744 | limit set by settings.min or settings.max. 745 |
touchspin.on.startspinTriggered when the spinner starts spinning upwards or downwards.
touchspin.on.startupspinTriggered when the spinner starts spinning upwards.
touchspin.on.startdownspinTriggered when the spinner starts spinning downwards.
touchspin.on.stopspinTriggered when the spinner stops spinning.
touchspin.on.stopupspinTriggered when the spinner stops upspinning.
touchspin.on.stopdownspinTriggered when the spinner stops downspinning.
touchspin.on.minTriggered when the spinner hits the limit set by settings.min.
touchspin.on.maxTriggered when the spinner hits the limit set by settings.max.
781 | 782 |

Callable events

783 | 784 |

The following events can be triggered on the original input.

785 | 786 |

787 | Example usages:
788 | $("input").trigger("touchspin.uponce");
789 | $("input").trigger("touchspin.updatesettings", {max: 1000}); 790 |

791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 |
EventDescription
touchspin.updatesettingsfunction(newoptions): Update any setting of an already initialized TouchSpin instance.
touchspin.uponceIncrease the value by one step.
touchspin.downonceDecrease the value by one step.
touchspin.startupspinStarts the spinner upwards.
touchspin.startdownspinStarts the spinner downwards.
touchspin.stopspinStops the spinner.
826 | 827 |

Download

828 | 829 |

Download from 830 | github. Please report issues and suggestions to github's issue tracker or contact me on 831 | g+ or 832 | twitter!

833 | 834 |
835 | 836 | 839 | 840 | 841 | 842 | -------------------------------------------------------------------------------- /dist/jquery.bootstrap-touchspin.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap Touchspin - v4.7.3 3 | * A mobile and touch friendly input spinner component for Bootstrap 3 & 4. 4 | * https://www.virtuosoft.eu/code/bootstrap-touchspin/ 5 | * 6 | * Made by István Ujj-Mészáros 7 | * Under MIT License 8 | */ 9 | /* This CSS file is unnecessary if you are not using vertical buttons functionality */ 10 | .bootstrap-touchspin .bootstrap-touchspin-vertical-button-wrapper { 11 | position: relative; 12 | width: 25px; 13 | border: none; 14 | } 15 | 16 | .bootstrap-touchspin .input-group-btn-vertical { 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | top: 0; 21 | bottom:0; 22 | z-index: 11; 23 | } 24 | 25 | .bootstrap-touchspin .input-group-btn-vertical > .btn { 26 | position: absolute; 27 | left: 0; 28 | right: 0; 29 | height: 50%; 30 | padding: 0; 31 | text-align: center; 32 | line-height: 1; 33 | } 34 | 35 | .bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up { 36 | border-radius: 0 4px 0 0; 37 | top: 0; 38 | } 39 | 40 | .bootstrap-touchspin .input-group-btn-vertical .btn { 41 | font-size: 12px; 42 | line-height: 1; 43 | } 44 | 45 | .rtl .bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up { 46 | border-radius: 4px 0 0 0; 47 | } 48 | 49 | .bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-up, 50 | .bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-down, 51 | .bootstrap-touchspin .input-group-btn:not(:last-child):not(:first-child) .btn { 52 | border-radius: 0; 53 | } 54 | 55 | .bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down { 56 | border-radius: 0 0 4px 0; 57 | bottom: 0; 58 | } 59 | 60 | 61 | .rtl .bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down { 62 | border-radius: 0 0 0 4px; 63 | } 64 | 65 | .bootstrap-touchspin .input-group-btn-vertical i { 66 | position: absolute; 67 | top: 3px; 68 | left: 5px; 69 | font-size: 9px; 70 | font-weight: normal; 71 | } 72 | 73 | .rtl .bootstrap-touchspin .input-group-btn-vertical i { 74 | left: auto; 75 | right: 5px; 76 | } 77 | -------------------------------------------------------------------------------- /dist/jquery.bootstrap-touchspin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } 4 | /* 5 | * Bootstrap Touchspin - v4.7.3 6 | * A mobile and touch friendly input spinner component for Bootstrap 3 & 4. 7 | * https://www.virtuosoft.eu/code/bootstrap-touchspin/ 8 | * 9 | * Made by István Ujj-Mészáros 10 | * Under MIT License 11 | */ 12 | (function (factory) { 13 | if (typeof define === 'function' && define.amd) { 14 | define(['jquery'], factory); 15 | } else if ((typeof module === "undefined" ? "undefined" : _typeof(module)) === 'object' && module.exports) { 16 | module.exports = function (root, jQuery) { 17 | if (jQuery === undefined) { 18 | if (typeof window !== 'undefined') { 19 | jQuery = require('jquery'); 20 | } else { 21 | jQuery = require('jquery')(root); 22 | } 23 | } 24 | factory(jQuery); 25 | return jQuery; 26 | }; 27 | } else { 28 | factory(jQuery); 29 | } 30 | })(function ($) { 31 | 'use strict'; 32 | 33 | var _currentSpinnerId = 0; 34 | $.fn.TouchSpin = function (options) { 35 | var defaults = { 36 | min: 0, 37 | // If null, there is no minimum enforced 38 | max: 100, 39 | // If null, there is no maximum enforced 40 | initval: '', 41 | replacementval: '', 42 | firstclickvalueifempty: null, 43 | step: 1, 44 | decimals: 0, 45 | stepinterval: 100, 46 | forcestepdivisibility: 'round', 47 | // none | floor | round | ceil 48 | stepintervaldelay: 500, 49 | verticalbuttons: false, 50 | verticalup: '+', 51 | verticaldown: '−', 52 | verticalupclass: '', 53 | verticaldownclass: '', 54 | prefix: '', 55 | postfix: '', 56 | prefix_extraclass: '', 57 | postfix_extraclass: '', 58 | booster: true, 59 | boostat: 10, 60 | maxboostedstep: false, 61 | mousewheel: true, 62 | buttondown_class: 'btn btn-primary', 63 | buttonup_class: 'btn btn-primary', 64 | buttondown_txt: '−', 65 | buttonup_txt: '+', 66 | callback_before_calculation: function callback_before_calculation(value) { 67 | return value; 68 | }, 69 | callback_after_calculation: function callback_after_calculation(value) { 70 | return value; 71 | } 72 | }; 73 | var attributeMap = { 74 | min: 'min', 75 | max: 'max', 76 | initval: 'init-val', 77 | replacementval: 'replacement-val', 78 | firstclickvalueifempty: 'first-click-value-if-empty', 79 | step: 'step', 80 | decimals: 'decimals', 81 | stepinterval: 'step-interval', 82 | verticalbuttons: 'vertical-buttons', 83 | verticalupclass: 'vertical-up-class', 84 | verticaldownclass: 'vertical-down-class', 85 | forcestepdivisibility: 'force-step-divisibility', 86 | stepintervaldelay: 'step-interval-delay', 87 | prefix: 'prefix', 88 | postfix: 'postfix', 89 | prefix_extraclass: 'prefix-extra-class', 90 | postfix_extraclass: 'postfix-extra-class', 91 | booster: 'booster', 92 | boostat: 'boostat', 93 | maxboostedstep: 'max-boosted-step', 94 | mousewheel: 'mouse-wheel', 95 | buttondown_class: 'button-down-class', 96 | buttonup_class: 'button-up-class', 97 | buttondown_txt: 'button-down-txt', 98 | buttonup_txt: 'button-up-txt' 99 | }; 100 | return this.each(function () { 101 | var settings, 102 | originalinput = $(this), 103 | originalinput_data = originalinput.data(), 104 | _detached_prefix, 105 | _detached_postfix, 106 | container, 107 | elements, 108 | verticalbuttons_html, 109 | value, 110 | downSpinTimer, 111 | upSpinTimer, 112 | downDelayTimeout, 113 | upDelayTimeout, 114 | spincount = 0, 115 | spinning = false; 116 | init(); 117 | function init() { 118 | if (originalinput.data('alreadyinitialized')) { 119 | return; 120 | } 121 | originalinput.data('alreadyinitialized', true); 122 | _currentSpinnerId += 1; 123 | originalinput.data('spinnerid', _currentSpinnerId); 124 | if (!originalinput.is('input')) { 125 | console.log('Must be an input.'); 126 | return; 127 | } 128 | _initSettings(); 129 | _setInitval(); 130 | _checkValue(); 131 | _buildHtml(); 132 | _initElements(); 133 | _updateButtonDisabledState(); 134 | _hideEmptyPrefixPostfix(); 135 | _setupMutationObservers(); 136 | _bindEvents(); 137 | _bindEventsInterface(); 138 | } 139 | function _setInitval() { 140 | if (settings.initval !== '' && originalinput.val() === '') { 141 | originalinput.val(settings.initval); 142 | } 143 | } 144 | function changeSettings(newsettings) { 145 | _updateSettings(newsettings); 146 | _checkValue(); 147 | var value = elements.input.val(); 148 | if (value !== '') { 149 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 150 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 151 | } 152 | } 153 | function _initSettings() { 154 | settings = $.extend({}, defaults, originalinput_data, _parseAttributes(), options); 155 | if (parseFloat(settings.step) !== 1) { 156 | var remainder; 157 | 158 | // Modify settings.max to be divisible by step 159 | remainder = settings.max % settings.step; 160 | if (remainder !== 0) { 161 | settings.max = parseFloat(settings.max) - remainder; 162 | } 163 | 164 | // Do the same with min, should work with negative numbers too 165 | remainder = settings.min % settings.step; 166 | if (remainder !== 0) { 167 | settings.min = parseFloat(settings.min) + (parseFloat(settings.step) - remainder); 168 | } 169 | } 170 | } 171 | function _parseAttributes() { 172 | var data = {}; 173 | 174 | // Setting up based on data attributes 175 | $.each(attributeMap, function (key, value) { 176 | var attrName = 'bts-' + value + ''; 177 | if (originalinput.is('[data-' + attrName + ']')) { 178 | data[key] = originalinput.data(attrName); 179 | } 180 | }); 181 | 182 | // Setting up based on input attributes if specified (input attributes have precedence) 183 | $.each(['min', 'max', 'step'], function (i, key) { 184 | if (originalinput.is('[' + key + ']')) { 185 | if (data[key] !== undefined) { 186 | console.warn('Both the "data-bts-' + key + '" data attribute and the "' + key + '" individual attribute were specified, the individual attribute will take precedence on: ', originalinput); 187 | } 188 | data[key] = originalinput.attr(key); 189 | } 190 | }); 191 | return data; 192 | } 193 | function _destroy() { 194 | var $parent = originalinput.parent(); 195 | stopSpin(); 196 | originalinput.off('.touchspin'); 197 | if ($parent.hasClass('bootstrap-touchspin-injected')) { 198 | originalinput.siblings().remove(); 199 | originalinput.unwrap(); 200 | } else { 201 | $('.bootstrap-touchspin-injected', $parent).remove(); 202 | $parent.removeClass('bootstrap-touchspin'); 203 | } 204 | originalinput.data('alreadyinitialized', false); 205 | } 206 | function _updateSettings(newsettings) { 207 | settings = $.extend({}, settings, newsettings); 208 | 209 | // Update postfix and prefix texts if those settings were changed. 210 | if (newsettings.postfix) { 211 | var $postfix = originalinput.parent().find('.bootstrap-touchspin-postfix'); 212 | if ($postfix.length === 0) { 213 | _detached_postfix.insertAfter(originalinput); 214 | } 215 | originalinput.parent().find('.bootstrap-touchspin-postfix .input-group-text').text(newsettings.postfix); 216 | } 217 | if (newsettings.prefix) { 218 | var $prefix = originalinput.parent().find('.bootstrap-touchspin-prefix'); 219 | if ($prefix.length === 0) { 220 | _detached_prefix.insertBefore(originalinput); 221 | } 222 | originalinput.parent().find('.bootstrap-touchspin-prefix .input-group-text').text(newsettings.prefix); 223 | } 224 | _hideEmptyPrefixPostfix(); 225 | } 226 | function _buildHtml() { 227 | var initval = originalinput.val(), 228 | parentelement = originalinput.parent(); 229 | if (initval !== '') { 230 | // initval may not be parsable as a number (callback_after_calculation() may decorate it so it cant be parsed). Use the callbacks if provided. 231 | initval = settings.callback_before_calculation(initval); 232 | initval = settings.callback_after_calculation(parseFloat(initval).toFixed(settings.decimals)); 233 | } 234 | originalinput.data('initvalue', initval).val(initval); 235 | originalinput.addClass('form-control'); 236 | verticalbuttons_html = "\n \n \n \n \n \n \n "); 237 | if (parentelement.hasClass('input-group')) { 238 | _advanceInputGroup(parentelement); 239 | } else { 240 | _buildInputGroup(); 241 | } 242 | } 243 | function _advanceInputGroup(parentelement) { 244 | parentelement.addClass('bootstrap-touchspin'); 245 | var prev = originalinput.prev(), 246 | next = originalinput.next(); 247 | var downhtml, 248 | uphtml, 249 | prefixhtml = "\n \n ".concat(settings.prefix, "\n \n "), 250 | postfixhtml = "\n \n ".concat(settings.postfix, "\n \n "); 251 | if (settings.verticalbuttons) { 252 | $(verticalbuttons_html).insertAfter(originalinput); 253 | } else { 254 | if (prev.hasClass('input-group-btn') || prev.hasClass('input-group-prepend')) { 255 | downhtml = "\n \n "); 256 | prev.append(downhtml); 257 | } else { 258 | downhtml = "\n \n \n \n "); 259 | $(downhtml).insertBefore(originalinput); 260 | } 261 | if (next.hasClass('input-group-btn') || next.hasClass('input-group-append')) { 262 | uphtml = "\n \n "); 263 | next.prepend(uphtml); 264 | } else { 265 | uphtml = "\n \n \n \n "); 266 | $(uphtml).insertAfter(originalinput); 267 | } 268 | } 269 | $(prefixhtml).insertBefore(originalinput); 270 | $(postfixhtml).insertAfter(originalinput); 271 | container = parentelement; 272 | } 273 | function _buildInputGroup() { 274 | var html; 275 | var inputGroupSize = ''; 276 | if (originalinput.hasClass('input-sm') || originalinput.hasClass('form-control-sm')) { 277 | inputGroupSize = 'input-group-sm'; 278 | } else if (originalinput.hasClass('input-lg') || originalinput.hasClass('form-control-lg')) { 279 | inputGroupSize = 'input-group-lg'; 280 | } 281 | if (settings.verticalbuttons) { 282 | html = "\n
\n \n ").concat(settings.prefix, "\n \n \n ").concat(settings.postfix, "\n \n ").concat(verticalbuttons_html, "\n
\n "); 283 | } else { 284 | html = "\n
\n \n \n \n \n ").concat(settings.prefix, "\n \n \n ").concat(settings.postfix, "\n \n \n \n \n
"); 285 | } 286 | container = $(html).insertBefore(originalinput); 287 | $('.bootstrap-touchspin-prefix', container).after(originalinput); 288 | if (originalinput.hasClass('input-sm') || originalinput.hasClass('form-control-sm')) { 289 | container.addClass('input-group-sm'); 290 | } else if (originalinput.hasClass('input-lg') || originalinput.hasClass('form-control-lg')) { 291 | container.addClass('input-group-lg'); 292 | } 293 | } 294 | function _initElements() { 295 | elements = { 296 | down: $('.bootstrap-touchspin-down', container), 297 | up: $('.bootstrap-touchspin-up', container), 298 | input: $('input', container), 299 | prefix: $('.bootstrap-touchspin-prefix', container).addClass(settings.prefix_extraclass), 300 | postfix: $('.bootstrap-touchspin-postfix', container).addClass(settings.postfix_extraclass) 301 | }; 302 | } 303 | function _hideEmptyPrefixPostfix() { 304 | if (settings.prefix === '') { 305 | _detached_prefix = elements.prefix.detach(); 306 | } 307 | if (settings.postfix === '') { 308 | _detached_postfix = elements.postfix.detach(); 309 | } 310 | } 311 | function _bindEvents() { 312 | originalinput.on('keydown.touchspin', function (ev) { 313 | var code = ev.keyCode || ev.which; 314 | if (code === 38) { 315 | if (spinning !== 'up') { 316 | upOnce(); 317 | startUpSpin(); 318 | } 319 | ev.preventDefault(); 320 | } else if (code === 40) { 321 | if (spinning !== 'down') { 322 | downOnce(); 323 | startDownSpin(); 324 | } 325 | ev.preventDefault(); 326 | } else if (code === 9 || code === 13) { 327 | _checkValue(); 328 | } 329 | }); 330 | originalinput.on('keyup.touchspin', function (ev) { 331 | var code = ev.keyCode || ev.which; 332 | if (code === 38) { 333 | stopSpin(); 334 | } else if (code === 40) { 335 | stopSpin(); 336 | } 337 | }); 338 | 339 | // change is fired before blur, so we need to work around that 340 | $(document).on('mousedown touchstart', function (event) { 341 | if ($(event.target).is(originalinput)) { 342 | return; 343 | } 344 | _checkValue(); 345 | }); 346 | originalinput.on('blur.touchspin', function () { 347 | _checkValue(); 348 | }); 349 | elements.down.on('keydown', function (ev) { 350 | var code = ev.keyCode || ev.which; 351 | if (code === 32 || code === 13) { 352 | if (spinning !== 'down') { 353 | downOnce(); 354 | startDownSpin(); 355 | } 356 | ev.preventDefault(); 357 | } 358 | }); 359 | elements.down.on('keyup.touchspin', function (ev) { 360 | var code = ev.keyCode || ev.which; 361 | if (code === 32 || code === 13) { 362 | stopSpin(); 363 | } 364 | }); 365 | elements.up.on('keydown.touchspin', function (ev) { 366 | var code = ev.keyCode || ev.which; 367 | if (code === 32 || code === 13) { 368 | if (spinning !== 'up') { 369 | upOnce(); 370 | startUpSpin(); 371 | } 372 | ev.preventDefault(); 373 | } 374 | }); 375 | elements.up.on('keyup.touchspin', function (ev) { 376 | var code = ev.keyCode || ev.which; 377 | if (code === 32 || code === 13) { 378 | stopSpin(); 379 | } 380 | }); 381 | elements.down.on('mousedown.touchspin', function (ev) { 382 | elements.down.off('touchstart.touchspin'); // android 4 workaround 383 | 384 | if (originalinput.is(':disabled,[readonly]')) { 385 | return; 386 | } 387 | downOnce(); 388 | startDownSpin(); 389 | ev.preventDefault(); 390 | ev.stopPropagation(); 391 | }); 392 | elements.down.on('touchstart.touchspin', function (ev) { 393 | elements.down.off('mousedown.touchspin'); // android 4 workaround 394 | 395 | if (originalinput.is(':disabled,[readonly]')) { 396 | return; 397 | } 398 | downOnce(); 399 | startDownSpin(); 400 | ev.preventDefault(); 401 | ev.stopPropagation(); 402 | }); 403 | elements.up.on('mousedown.touchspin', function (ev) { 404 | elements.up.off('touchstart.touchspin'); // android 4 workaround 405 | 406 | if (originalinput.is(':disabled,[readonly]')) { 407 | return; 408 | } 409 | upOnce(); 410 | startUpSpin(); 411 | ev.preventDefault(); 412 | ev.stopPropagation(); 413 | }); 414 | elements.up.on('touchstart.touchspin', function (ev) { 415 | elements.up.off('mousedown.touchspin'); // android 4 workaround 416 | 417 | if (originalinput.is(':disabled,[readonly]')) { 418 | return; 419 | } 420 | upOnce(); 421 | startUpSpin(); 422 | ev.preventDefault(); 423 | ev.stopPropagation(); 424 | }); 425 | elements.up.on('mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin', function (ev) { 426 | if (!spinning) { 427 | return; 428 | } 429 | ev.stopPropagation(); 430 | stopSpin(); 431 | }); 432 | elements.down.on('mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin', function (ev) { 433 | if (!spinning) { 434 | return; 435 | } 436 | ev.stopPropagation(); 437 | stopSpin(); 438 | }); 439 | elements.down.on('mousemove.touchspin touchmove.touchspin', function (ev) { 440 | if (!spinning) { 441 | return; 442 | } 443 | ev.stopPropagation(); 444 | ev.preventDefault(); 445 | }); 446 | elements.up.on('mousemove.touchspin touchmove.touchspin', function (ev) { 447 | if (!spinning) { 448 | return; 449 | } 450 | ev.stopPropagation(); 451 | ev.preventDefault(); 452 | }); 453 | originalinput.on('mousewheel.touchspin DOMMouseScroll.touchspin', function (ev) { 454 | if (!settings.mousewheel || !originalinput.is(':focus')) { 455 | return; 456 | } 457 | var delta = ev.originalEvent.wheelDelta || -ev.originalEvent.deltaY || -ev.originalEvent.detail; 458 | ev.stopPropagation(); 459 | ev.preventDefault(); 460 | if (delta < 0) { 461 | downOnce(); 462 | } else { 463 | upOnce(); 464 | } 465 | }); 466 | } 467 | function _bindEventsInterface() { 468 | originalinput.on('touchspin.destroy', function () { 469 | _destroy(); 470 | }); 471 | originalinput.on('touchspin.uponce', function () { 472 | stopSpin(); 473 | upOnce(); 474 | }); 475 | originalinput.on('touchspin.downonce', function () { 476 | stopSpin(); 477 | downOnce(); 478 | }); 479 | originalinput.on('touchspin.startupspin', function () { 480 | startUpSpin(); 481 | }); 482 | originalinput.on('touchspin.startdownspin', function () { 483 | startDownSpin(); 484 | }); 485 | originalinput.on('touchspin.stopspin', function () { 486 | stopSpin(); 487 | }); 488 | originalinput.on('touchspin.updatesettings', function (e, newsettings) { 489 | changeSettings(newsettings); 490 | }); 491 | } 492 | function _setupMutationObservers() { 493 | if (typeof MutationObserver !== 'undefined') { 494 | // MutationObserver is available 495 | var observer = new MutationObserver(function (mutations) { 496 | mutations.forEach(function (mutation) { 497 | if (mutation.type === 'attributes' && (mutation.attributeName === 'disabled' || mutation.attributeName === 'readonly')) { 498 | _updateButtonDisabledState(); 499 | } 500 | }); 501 | }); 502 | observer.observe(originalinput[0], { 503 | attributes: true 504 | }); 505 | } 506 | } 507 | function _forcestepdivisibility(value) { 508 | switch (settings.forcestepdivisibility) { 509 | case 'round': 510 | return (Math.round(value / settings.step) * settings.step).toFixed(settings.decimals); 511 | case 'floor': 512 | return (Math.floor(value / settings.step) * settings.step).toFixed(settings.decimals); 513 | case 'ceil': 514 | return (Math.ceil(value / settings.step) * settings.step).toFixed(settings.decimals); 515 | default: 516 | return value.toFixed(settings.decimals); 517 | } 518 | } 519 | function _checkValue() { 520 | var val, parsedval, returnval; 521 | val = settings.callback_before_calculation(originalinput.val()); 522 | if (val === '') { 523 | if (settings.replacementval !== '') { 524 | originalinput.val(settings.replacementval); 525 | originalinput.trigger('change'); 526 | } 527 | return; 528 | } 529 | if (settings.decimals > 0 && val === '.') { 530 | return; 531 | } 532 | parsedval = parseFloat(val); 533 | if (isNaN(parsedval)) { 534 | if (settings.replacementval !== '') { 535 | parsedval = settings.replacementval; 536 | } else { 537 | parsedval = 0; 538 | } 539 | } 540 | returnval = parsedval; 541 | if (parsedval.toString() !== val) { 542 | returnval = parsedval; 543 | } 544 | returnval = _forcestepdivisibility(parsedval); 545 | if (settings.min !== null && parsedval < settings.min) { 546 | returnval = settings.min; 547 | } 548 | if (settings.max !== null && parsedval > settings.max) { 549 | returnval = settings.max; 550 | } 551 | if (parseFloat(parsedval).toString() !== parseFloat(returnval).toString()) { 552 | originalinput.val(returnval); 553 | } 554 | originalinput.val(settings.callback_after_calculation(parseFloat(returnval).toFixed(settings.decimals))); 555 | } 556 | function _getBoostedStep() { 557 | if (!settings.booster) { 558 | return settings.step; 559 | } else { 560 | var boosted = Math.pow(2, Math.floor(spincount / settings.boostat)) * settings.step; 561 | if (settings.maxboostedstep) { 562 | if (boosted > settings.maxboostedstep) { 563 | boosted = settings.maxboostedstep; 564 | value = Math.round(value / boosted) * boosted; 565 | } 566 | } 567 | return Math.max(settings.step, boosted); 568 | } 569 | } 570 | function valueIfIsNaN() { 571 | if (typeof settings.firstclickvalueifempty === 'number') { 572 | return settings.firstclickvalueifempty; 573 | } else { 574 | return (settings.min + settings.max) / 2; 575 | } 576 | } 577 | function _updateButtonDisabledState() { 578 | var isDisabled = originalinput.is(':disabled,[readonly]'); 579 | elements.up.prop('disabled', isDisabled); 580 | elements.down.prop('disabled', isDisabled); 581 | if (isDisabled) { 582 | stopSpin(); 583 | } 584 | } 585 | function upOnce() { 586 | if (originalinput.is(':disabled,[readonly]')) { 587 | return; 588 | } 589 | _checkValue(); 590 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 591 | var initvalue = value; 592 | var boostedstep; 593 | if (isNaN(value)) { 594 | value = valueIfIsNaN(); 595 | } else { 596 | boostedstep = _getBoostedStep(); 597 | value = value + boostedstep; 598 | } 599 | if (settings.max !== null && value >= settings.max) { 600 | value = settings.max; 601 | originalinput.trigger('touchspin.on.max'); 602 | stopSpin(); 603 | } 604 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 605 | if (initvalue !== value) { 606 | originalinput.trigger('change'); 607 | } 608 | } 609 | function downOnce() { 610 | if (originalinput.is(':disabled,[readonly]')) { 611 | return; 612 | } 613 | _checkValue(); 614 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 615 | var initvalue = value; 616 | var boostedstep; 617 | if (isNaN(value)) { 618 | value = valueIfIsNaN(); 619 | } else { 620 | boostedstep = _getBoostedStep(); 621 | value = value - boostedstep; 622 | } 623 | if (settings.min !== null && value <= settings.min) { 624 | value = settings.min; 625 | originalinput.trigger('touchspin.on.min'); 626 | stopSpin(); 627 | } 628 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 629 | if (initvalue !== value) { 630 | originalinput.trigger('change'); 631 | } 632 | } 633 | function startDownSpin() { 634 | if (originalinput.is(':disabled,[readonly]')) { 635 | return; 636 | } 637 | stopSpin(); 638 | spincount = 0; 639 | spinning = 'down'; 640 | originalinput.trigger('touchspin.on.startspin'); 641 | originalinput.trigger('touchspin.on.startdownspin'); 642 | downDelayTimeout = setTimeout(function () { 643 | downSpinTimer = setInterval(function () { 644 | spincount++; 645 | downOnce(); 646 | }, settings.stepinterval); 647 | }, settings.stepintervaldelay); 648 | } 649 | function startUpSpin() { 650 | if (originalinput.is(':disabled,[readonly]')) { 651 | return; 652 | } 653 | stopSpin(); 654 | spincount = 0; 655 | spinning = 'up'; 656 | originalinput.trigger('touchspin.on.startspin'); 657 | originalinput.trigger('touchspin.on.startupspin'); 658 | upDelayTimeout = setTimeout(function () { 659 | upSpinTimer = setInterval(function () { 660 | spincount++; 661 | upOnce(); 662 | }, settings.stepinterval); 663 | }, settings.stepintervaldelay); 664 | } 665 | function stopSpin() { 666 | clearTimeout(downDelayTimeout); 667 | clearTimeout(upDelayTimeout); 668 | clearInterval(downSpinTimer); 669 | clearInterval(upSpinTimer); 670 | switch (spinning) { 671 | case 'up': 672 | originalinput.trigger('touchspin.on.stopupspin'); 673 | originalinput.trigger('touchspin.on.stopspin'); 674 | break; 675 | case 'down': 676 | originalinput.trigger('touchspin.on.stopdownspin'); 677 | originalinput.trigger('touchspin.on.stopspin'); 678 | break; 679 | } 680 | spincount = 0; 681 | spinning = false; 682 | } 683 | }); 684 | }; 685 | }); 686 | //# sourceMappingURL=jquery.bootstrap-touchspin.js.map 687 | -------------------------------------------------------------------------------- /dist/jquery.bootstrap-touchspin.min.css: -------------------------------------------------------------------------------- 1 | .bootstrap-touchspin .bootstrap-touchspin-vertical-button-wrapper{position:relative;width:25px;border:none}.bootstrap-touchspin .input-group-btn-vertical{position:absolute;left:0;right:0;top:0;bottom:0;z-index:11}.bootstrap-touchspin .input-group-btn-vertical>.btn{position:absolute;left:0;right:0;height:50%;padding:0;text-align:center;line-height:1}.bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up{border-radius:0 4px 0 0;top:0}.bootstrap-touchspin .input-group-btn-vertical .btn{font-size:12px;line-height:1}.rtl .bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up{border-radius:4px 0 0 0}.bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-down,.bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-up,.bootstrap-touchspin .input-group-btn:not(:last-child):not(:first-child) .btn{border-radius:0}.bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down{border-radius:0 0 4px 0;bottom:0}.rtl .bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down{border-radius:0 0 0 4px}.bootstrap-touchspin .input-group-btn-vertical i{position:absolute;top:3px;left:5px;font-size:9px;font-weight:400}.rtl .bootstrap-touchspin .input-group-btn-vertical i{left:auto;right:5px} -------------------------------------------------------------------------------- /dist/jquery.bootstrap-touchspin.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap Touchspin - v4.7.3 3 | * A mobile and touch friendly input spinner component for Bootstrap 3 & 4. 4 | * https://www.virtuosoft.eu/code/bootstrap-touchspin/ 5 | * 6 | * Made by István Ujj-Mészáros 7 | * Under MIT License 8 | */ 9 | function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}(o=>{"function"==typeof define&&define.amd?define(["jquery"],o):"object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=function(t,n){return void 0===n&&(n="undefined"!=typeof window?require("jquery"):require("jquery")(t)),o(n),n}:o(jQuery)})(function(B){var T=0;B.fn.TouchSpin=function(P){var N={min:0,max:100,initval:"",replacementval:"",firstclickvalueifempty:null,step:1,decimals:0,stepinterval:100,forcestepdivisibility:"round",stepintervaldelay:500,verticalbuttons:!1,verticalup:"+",verticaldown:"−",verticalupclass:"",verticaldownclass:"",prefix:"",postfix:"",prefix_extraclass:"",postfix_extraclass:"",booster:!0,boostat:10,maxboostedstep:!1,mousewheel:!0,buttondown_class:"btn btn-primary",buttonup_class:"btn btn-primary",buttondown_txt:"−",buttonup_txt:"+",callback_before_calculation:function(t){return t},callback_after_calculation:function(t){return t}},q={min:"min",max:"max",initval:"init-val",replacementval:"replacement-val",firstclickvalueifempty:"first-click-value-if-empty",step:"step",decimals:"decimals",stepinterval:"step-interval",verticalbuttons:"vertical-buttons",verticalupclass:"vertical-up-class",verticaldownclass:"vertical-down-class",forcestepdivisibility:"force-step-divisibility",stepintervaldelay:"step-interval-delay",prefix:"prefix",postfix:"postfix",prefix_extraclass:"prefix-extra-class",postfix_extraclass:"postfix-extra-class",booster:"booster",boostat:"boostat",maxboostedstep:"max-boosted-step",mousewheel:"mouse-wheel",buttondown_class:"button-down-class",buttonup_class:"button-up-class",buttondown_txt:"button-down-txt",buttonup_txt:"button-up-txt"};return this.each(function(){var a,e,s,p,t,o,n,i,u,c,r,l,d,f,b,h,m=B(this),v=m.data(),x=0,g=!1;function y(){""===a.prefix&&(e=p.prefix.detach()),""===a.postfix&&(s=p.postfix.detach())}function w(){var t,n,o=a.callback_before_calculation(m.val());""===o?""!==a.replacementval&&(m.val(a.replacementval),m.trigger("change")):0{switch(a.forcestepdivisibility){case"round":return(Math.round(t/a.step)*a.step).toFixed(a.decimals);case"floor":return(Math.floor(t/a.step)*a.step).toFixed(a.decimals);case"ceil":return(Math.ceil(t/a.step)*a.step).toFixed(a.decimals);default:return t.toFixed(a.decimals)}})(t),null!==a.min&&ta.max&&(n=a.max),parseFloat(t).toString()!==parseFloat(n).toString()&&m.val(n),m.val(a.callback_after_calculation(parseFloat(n).toFixed(a.decimals))))}function _(){var t;return a.booster?(t=Math.pow(2,Math.floor(x/a.boostat))*a.step,a.maxboostedstep&&t>a.maxboostedstep&&(t=a.maxboostedstep,o=Math.round(o/t)*t),Math.max(a.step,t)):a.step}function k(){return"number"==typeof a.firstclickvalueifempty?a.firstclickvalueifempty:(a.min+a.max)/2}function C(){var t=m.is(":disabled,[readonly]");p.up.prop("disabled",t),p.down.prop("disabled",t),t&&S()}function F(){var t,n;m.is(":disabled,[readonly]")||(w(),t=o=parseFloat(a.callback_before_calculation(p.input.val())),isNaN(o)?o=k():(n=_(),o+=n),null!==a.max&&o>=a.max&&(o=a.max,m.trigger("touchspin.on.max"),S()),p.input.val(a.callback_after_calculation(parseFloat(o).toFixed(a.decimals))),t!==o&&m.trigger("change"))}function j(){var t,n;m.is(":disabled,[readonly]")||(w(),t=o=parseFloat(a.callback_before_calculation(p.input.val())),isNaN(o)?o=k():(n=_(),o-=n),null!==a.min&&o<=a.min&&(o=a.min,m.trigger("touchspin.on.min"),S()),p.input.val(a.callback_after_calculation(parseFloat(o).toFixed(a.decimals))),t!==o&&m.trigger("change"))}function D(){m.is(":disabled,[readonly]")||(S(),x=0,g="down",m.trigger("touchspin.on.startspin"),m.trigger("touchspin.on.startdownspin"),u=setTimeout(function(){n=setInterval(function(){x++,j()},a.stepinterval)},a.stepintervaldelay))}function M(){m.is(":disabled,[readonly]")||(S(),x=0,g="up",m.trigger("touchspin.on.startspin"),m.trigger("touchspin.on.startupspin"),c=setTimeout(function(){i=setInterval(function(){x++,F()},a.stepinterval)},a.stepintervaldelay))}function S(){switch(clearTimeout(u),clearTimeout(c),clearInterval(n),clearInterval(i),g){case"up":m.trigger("touchspin.on.stopupspin"),m.trigger("touchspin.on.stopspin");break;case"down":m.trigger("touchspin.on.stopdownspin"),m.trigger("touchspin.on.stopspin")}x=0,g=!1}m.data("alreadyinitialized")||(m.data("alreadyinitialized",!0),T+=1,m.data("spinnerid",T),m.is("input")?(a=B.extend({},N,v,(()=>{var o={};return B.each(q,function(t,n){n="bts-"+n;m.is("[data-"+n+"]")&&(o[t]=m.data(n))}),B.each(["min","max","step"],function(t,n){m.is("["+n+"]")&&(void 0!==o[n]&&console.warn('Both the "data-bts-'+n+'" data attribute and the "'+n+'" individual attribute were specified, the individual attribute will take precedence on: ',m),o[n]=m.attr(n))}),o})(),P),1!==parseFloat(a.step)&&(0!=(h=a.max%a.step)&&(a.max=parseFloat(a.max)-h),0!=(h=a.min%a.step))&&(a.min=parseFloat(a.min)+(parseFloat(a.step)-h)),""!==a.initval&&""===m.val()&&m.val(a.initval),w(),h=m.val(),v=m.parent(),""!==h&&(h=a.callback_before_calculation(h),h=a.callback_after_calculation(parseFloat(h).toFixed(a.decimals))),m.data("initvalue",h).val(h),m.addClass("form-control"),t='\n \n \n \n \n \n \n "),v.hasClass("input-group")?((h=v).addClass("bootstrap-touchspin"),v=m.prev(),d=m.next(),f='\n \n '.concat(a.prefix,"\n \n "),b='\n \n '.concat(a.postfix,"\n \n "),a.verticalbuttons?B(t).insertAfter(m):(v.hasClass("input-group-btn")||v.hasClass("input-group-prepend")?(r='\n \n "),v.append(r)):(r='\n \n \n \n "),B(r).insertBefore(m)),d.hasClass("input-group-btn")||d.hasClass("input-group-append")?(l='\n \n "),d.prepend(l)):(l='\n \n \n \n "),B(l).insertAfter(m))),B(f).insertBefore(m),B(b).insertAfter(m),r=h):(v="",m.hasClass("input-sm")||m.hasClass("form-control-sm")?v="input-group-sm":(m.hasClass("input-lg")||m.hasClass("form-control-lg"))&&(v="input-group-lg"),v=a.verticalbuttons?'\n
\n \n ').concat(a.prefix,'\n \n \n ').concat(a.postfix,"\n \n ").concat(t,"\n
\n "):'\n
\n \n \n \n \n ').concat(a.prefix,'\n \n \n ').concat(a.postfix,'\n \n \n \n \n
"),r=B(v).insertBefore(m),B(".bootstrap-touchspin-prefix",r).after(m),m.hasClass("input-sm")||m.hasClass("form-control-sm")?r.addClass("input-group-sm"):(m.hasClass("input-lg")||m.hasClass("form-control-lg"))&&r.addClass("input-group-lg")),p={down:B(".bootstrap-touchspin-down",r),up:B(".bootstrap-touchspin-up",r),input:B("input",r),prefix:B(".bootstrap-touchspin-prefix",r).addClass(a.prefix_extraclass),postfix:B(".bootstrap-touchspin-postfix",r).addClass(a.postfix_extraclass)},C(),y(),"undefined"!=typeof MutationObserver&&new MutationObserver(function(t){t.forEach(function(t){"attributes"!==t.type||"disabled"!==t.attributeName&&"readonly"!==t.attributeName||C()})}).observe(m[0],{attributes:!0}),m.on("keydown.touchspin",function(t){var n=t.keyCode||t.which;38===n?("up"!==g&&(F(),M()),t.preventDefault()):40===n?("down"!==g&&(j(),D()),t.preventDefault()):9!==n&&13!==n||w()}),m.on("keyup.touchspin",function(t){t=t.keyCode||t.which;38!==t&&40!==t||S()}),B(document).on("mousedown touchstart",function(t){B(t.target).is(m)||w()}),m.on("blur.touchspin",function(){w()}),p.down.on("keydown",function(t){var n=t.keyCode||t.which;32!==n&&13!==n||("down"!==g&&(j(),D()),t.preventDefault())}),p.down.on("keyup.touchspin",function(t){t=t.keyCode||t.which;32!==t&&13!==t||S()}),p.up.on("keydown.touchspin",function(t){var n=t.keyCode||t.which;32!==n&&13!==n||("up"!==g&&(F(),M()),t.preventDefault())}),p.up.on("keyup.touchspin",function(t){t=t.keyCode||t.which;32!==t&&13!==t||S()}),p.down.on("mousedown.touchspin",function(t){p.down.off("touchstart.touchspin"),m.is(":disabled,[readonly]")||(j(),D(),t.preventDefault(),t.stopPropagation())}),p.down.on("touchstart.touchspin",function(t){p.down.off("mousedown.touchspin"),m.is(":disabled,[readonly]")||(j(),D(),t.preventDefault(),t.stopPropagation())}),p.up.on("mousedown.touchspin",function(t){p.up.off("touchstart.touchspin"),m.is(":disabled,[readonly]")||(F(),M(),t.preventDefault(),t.stopPropagation())}),p.up.on("touchstart.touchspin",function(t){p.up.off("mousedown.touchspin"),m.is(":disabled,[readonly]")||(F(),M(),t.preventDefault(),t.stopPropagation())}),p.up.on("mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin",function(t){g&&(t.stopPropagation(),S())}),p.down.on("mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin",function(t){g&&(t.stopPropagation(),S())}),p.down.on("mousemove.touchspin touchmove.touchspin",function(t){g&&(t.stopPropagation(),t.preventDefault())}),p.up.on("mousemove.touchspin touchmove.touchspin",function(t){g&&(t.stopPropagation(),t.preventDefault())}),m.on("mousewheel.touchspin DOMMouseScroll.touchspin",function(t){var n;a.mousewheel&&m.is(":focus")&&(n=t.originalEvent.wheelDelta||-t.originalEvent.deltaY||-t.originalEvent.detail,t.stopPropagation(),t.preventDefault(),(n<0?j:F)())}),m.on("touchspin.destroy",function(){var t=m.parent();S(),m.off(".touchspin"),t.hasClass("bootstrap-touchspin-injected")?(m.siblings().remove(),m.unwrap()):(B(".bootstrap-touchspin-injected",t).remove(),t.removeClass("bootstrap-touchspin")),m.data("alreadyinitialized",!1)}),m.on("touchspin.uponce",function(){S(),F()}),m.on("touchspin.downonce",function(){S(),j()}),m.on("touchspin.startupspin",function(){M()}),m.on("touchspin.startdownspin",function(){D()}),m.on("touchspin.stopspin",function(){S()}),m.on("touchspin.updatesettings",function(t,n){var o=n;a=B.extend({},a,o),o.postfix&&(0===m.parent().find(".bootstrap-touchspin-postfix").length&&s.insertAfter(m),m.parent().find(".bootstrap-touchspin-postfix .input-group-text").text(o.postfix)),o.prefix&&(0===m.parent().find(".bootstrap-touchspin-prefix").length&&e.insertBefore(m),m.parent().find(".bootstrap-touchspin-prefix .input-group-text").text(o.prefix)),y(),w(),""!==(n=p.input.val())&&(n=parseFloat(a.callback_before_calculation(p.input.val())),p.input.val(a.callback_after_calculation(parseFloat(n).toFixed(a.decimals))))})):console.log("Must be an input."))})}}); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | process.setMaxListeners(20); 2 | 3 | module.exports = { 4 | 'preset': 'jest-puppeteer', 5 | 'testMatch': [ 6 | '**/?(*.)+(spec|test).[jt]s?(x)' 7 | ], 8 | 'testTimeout': 50000, 9 | transform: { 10 | '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-touchspin", 3 | "version": "4.7.3", 4 | "title": "Bootstrap Touchspin", 5 | "description": "A mobile and touch friendly input spinner component for Bootstrap 3 & 4.", 6 | "keywords": [ 7 | "input", 8 | "bootstrap", 9 | "number", 10 | "range", 11 | "spinbutton", 12 | "spinner" 13 | ], 14 | "author": { 15 | "name": "István Ujj-Mészáros", 16 | "url": "https://github.com/istvan-ujjmeszaros" 17 | }, 18 | "main": "dist/jquery.bootstrap-touchspin.js", 19 | "license": "MIT", 20 | "contributors": [], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/istvan-ujjmeszaros/bootstrap-touchspin.git" 24 | }, 25 | "homepage": "https://www.virtuosoft.eu/code/bootstrap-touchspin/", 26 | "demo": "https://www.virtuosoft.eu/code/bootstrap-touchspin/", 27 | "docs": "https://www.virtuosoft.eu/code/bootstrap-touchspin/", 28 | "download": "https://github.com/istvan-ujjmeszaros/bootstrap-touchspin/archive/master.zip", 29 | "devDependencies": { 30 | "@babel/core": "^7.26.0", 31 | "@babel/preset-env": "^7.26.0", 32 | "@types/express": "^5.0.0", 33 | "@types/jest": "^29.5.14", 34 | "@types/jest-environment-puppeteer": "^5.0.6", 35 | "express": "^4.21.2", 36 | "grunt": "^1.6.1", 37 | "grunt-babel": "^8.0.0", 38 | "grunt-cli": "^1.5.0", 39 | "grunt-contrib-clean": "^2.0.1", 40 | "grunt-contrib-concat": "^2.1.0", 41 | "grunt-contrib-cssmin": "^5.0.0", 42 | "grunt-contrib-jshint": "^3.2.0", 43 | "grunt-contrib-uglify": "^5.2.2", 44 | "jest": "^29.7.0", 45 | "jest-puppeteer": "^11.0.0", 46 | "puppeteer": "^23.11.1", 47 | "ts-jest": "^29.2.5", 48 | "typescript": "^5.7.2" 49 | }, 50 | "scripts": { 51 | "test": "jest --runInBand", 52 | "build": "grunt default", 53 | "check-build-integrity": "grunt check-build-integrity" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/jquery.bootstrap-touchspin.css: -------------------------------------------------------------------------------- 1 | /* This CSS file is unnecessary if you are not using vertical buttons functionality */ 2 | .bootstrap-touchspin .bootstrap-touchspin-vertical-button-wrapper { 3 | position: relative; 4 | width: 25px; 5 | border: none; 6 | } 7 | 8 | .bootstrap-touchspin .input-group-btn-vertical { 9 | position: absolute; 10 | left: 0; 11 | right: 0; 12 | top: 0; 13 | bottom:0; 14 | z-index: 11; 15 | } 16 | 17 | .bootstrap-touchspin .input-group-btn-vertical > .btn { 18 | position: absolute; 19 | left: 0; 20 | right: 0; 21 | height: 50%; 22 | padding: 0; 23 | text-align: center; 24 | line-height: 1; 25 | } 26 | 27 | .bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up { 28 | border-radius: 0 4px 0 0; 29 | top: 0; 30 | } 31 | 32 | .bootstrap-touchspin .input-group-btn-vertical .btn { 33 | font-size: 12px; 34 | line-height: 1; 35 | } 36 | 37 | .rtl .bootstrap-touchspin .input-group-addon .input-group-btn-vertical .bootstrap-touchspin-up { 38 | border-radius: 4px 0 0 0; 39 | } 40 | 41 | .bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-up, 42 | .bootstrap-touchspin .input-group-addon:not(:last-child) .input-group-btn-vertical .bootstrap-touchspin-down, 43 | .bootstrap-touchspin .input-group-btn:not(:last-child):not(:first-child) .btn { 44 | border-radius: 0; 45 | } 46 | 47 | .bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down { 48 | border-radius: 0 0 4px 0; 49 | bottom: 0; 50 | } 51 | 52 | 53 | .rtl .bootstrap-touchspin .input-group-btn-vertical .bootstrap-touchspin-down { 54 | border-radius: 0 0 0 4px; 55 | } 56 | 57 | .bootstrap-touchspin .input-group-btn-vertical i { 58 | position: absolute; 59 | top: 3px; 60 | left: 5px; 61 | font-size: 9px; 62 | font-weight: normal; 63 | } 64 | 65 | .rtl .bootstrap-touchspin .input-group-btn-vertical i { 66 | left: auto; 67 | right: 5px; 68 | } 69 | -------------------------------------------------------------------------------- /src/jquery.bootstrap-touchspin.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['jquery'], factory); 4 | } else if (typeof module === 'object' && module.exports) { 5 | module.exports = function (root, jQuery) { 6 | if (jQuery === undefined) { 7 | if (typeof window !== 'undefined') { 8 | jQuery = require('jquery'); 9 | } else { 10 | jQuery = require('jquery')(root); 11 | } 12 | } 13 | factory(jQuery); 14 | return jQuery; 15 | }; 16 | } else { 17 | factory(jQuery); 18 | } 19 | }(function ($) { 20 | 'use strict'; 21 | 22 | var _currentSpinnerId = 0; 23 | 24 | $.fn.TouchSpin = function (options) { 25 | 26 | var defaults = { 27 | min: 0, // If null, there is no minimum enforced 28 | max: 100, // If null, there is no maximum enforced 29 | initval: '', 30 | replacementval: '', 31 | firstclickvalueifempty: null, 32 | step: 1, 33 | decimals: 0, 34 | stepinterval: 100, 35 | forcestepdivisibility: 'round', // none | floor | round | ceil 36 | stepintervaldelay: 500, 37 | verticalbuttons: false, 38 | verticalup: '+', 39 | verticaldown: '−', 40 | verticalupclass: '', 41 | verticaldownclass: '', 42 | prefix: '', 43 | postfix: '', 44 | prefix_extraclass: '', 45 | postfix_extraclass: '', 46 | booster: true, 47 | boostat: 10, 48 | maxboostedstep: false, 49 | mousewheel: true, 50 | buttondown_class: 'btn btn-primary', 51 | buttonup_class: 'btn btn-primary', 52 | buttondown_txt: '−', 53 | buttonup_txt: '+', 54 | callback_before_calculation: function (value) { 55 | return value; 56 | }, 57 | callback_after_calculation: function (value) { 58 | return value; 59 | } 60 | }; 61 | 62 | var attributeMap = { 63 | min: 'min', 64 | max: 'max', 65 | initval: 'init-val', 66 | replacementval: 'replacement-val', 67 | firstclickvalueifempty: 'first-click-value-if-empty', 68 | step: 'step', 69 | decimals: 'decimals', 70 | stepinterval: 'step-interval', 71 | verticalbuttons: 'vertical-buttons', 72 | verticalupclass: 'vertical-up-class', 73 | verticaldownclass: 'vertical-down-class', 74 | forcestepdivisibility: 'force-step-divisibility', 75 | stepintervaldelay: 'step-interval-delay', 76 | prefix: 'prefix', 77 | postfix: 'postfix', 78 | prefix_extraclass: 'prefix-extra-class', 79 | postfix_extraclass: 'postfix-extra-class', 80 | booster: 'booster', 81 | boostat: 'boostat', 82 | maxboostedstep: 'max-boosted-step', 83 | mousewheel: 'mouse-wheel', 84 | buttondown_class: 'button-down-class', 85 | buttonup_class: 'button-up-class', 86 | buttondown_txt: 'button-down-txt', 87 | buttonup_txt: 'button-up-txt' 88 | }; 89 | 90 | return this.each(function () { 91 | 92 | var settings, 93 | originalinput = $(this), 94 | originalinput_data = originalinput.data(), 95 | _detached_prefix, 96 | _detached_postfix, 97 | container, 98 | elements, 99 | verticalbuttons_html, 100 | value, 101 | downSpinTimer, 102 | upSpinTimer, 103 | downDelayTimeout, 104 | upDelayTimeout, 105 | spincount = 0, 106 | spinning = false; 107 | 108 | init(); 109 | 110 | function init() { 111 | if (originalinput.data('alreadyinitialized')) { 112 | return; 113 | } 114 | 115 | originalinput.data('alreadyinitialized', true); 116 | _currentSpinnerId += 1; 117 | originalinput.data('spinnerid', _currentSpinnerId); 118 | 119 | if (!originalinput.is('input')) { 120 | console.log('Must be an input.'); 121 | return; 122 | } 123 | 124 | _initSettings(); 125 | _setInitval(); 126 | _checkValue(); 127 | _buildHtml(); 128 | _initElements(); 129 | _updateButtonDisabledState(); 130 | _hideEmptyPrefixPostfix(); 131 | _setupMutationObservers(); 132 | _bindEvents(); 133 | _bindEventsInterface(); 134 | } 135 | 136 | function _setInitval() { 137 | if (settings.initval !== '' && originalinput.val() === '') { 138 | originalinput.val(settings.initval); 139 | } 140 | } 141 | 142 | function changeSettings(newsettings) { 143 | _updateSettings(newsettings); 144 | _checkValue(); 145 | 146 | var value = elements.input.val(); 147 | 148 | if (value !== '') { 149 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 150 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 151 | } 152 | } 153 | 154 | function _initSettings() { 155 | settings = $.extend({}, defaults, originalinput_data, _parseAttributes(), options); 156 | 157 | if (parseFloat(settings.step) !== 1) { 158 | let remainder; 159 | 160 | // Modify settings.max to be divisible by step 161 | remainder = settings.max % settings.step; 162 | if (remainder !== 0) { 163 | settings.max = parseFloat(settings.max) - remainder; 164 | } 165 | 166 | // Do the same with min, should work with negative numbers too 167 | remainder = settings.min % settings.step; 168 | if (remainder !== 0) { 169 | settings.min = parseFloat(settings.min) + (parseFloat(settings.step) - remainder); 170 | } 171 | } 172 | } 173 | 174 | function _parseAttributes() { 175 | var data = {}; 176 | 177 | // Setting up based on data attributes 178 | $.each(attributeMap, function (key, value) { 179 | var attrName = 'bts-' + value + ''; 180 | 181 | if (originalinput.is('[data-' + attrName + ']')) { 182 | data[key] = originalinput.data(attrName); 183 | } 184 | }); 185 | 186 | // Setting up based on input attributes if specified (input attributes have precedence) 187 | $.each(['min', 'max', 'step'], function (i, key) { 188 | if (originalinput.is('['+key+']')) { 189 | if (data[key] !== undefined) { 190 | console.warn('Both the "data-bts-' + key + '" data attribute and the "' + key + '" individual attribute were specified, the individual attribute will take precedence on: ', originalinput); 191 | } 192 | data[key] = originalinput.attr(key); 193 | } 194 | }); 195 | 196 | return data; 197 | } 198 | 199 | function _destroy() { 200 | var $parent = originalinput.parent(); 201 | 202 | stopSpin(); 203 | 204 | originalinput.off('.touchspin'); 205 | 206 | if ($parent.hasClass('bootstrap-touchspin-injected')) { 207 | originalinput.siblings().remove(); 208 | originalinput.unwrap(); 209 | } else { 210 | $('.bootstrap-touchspin-injected', $parent).remove(); 211 | $parent.removeClass('bootstrap-touchspin'); 212 | } 213 | 214 | originalinput.data('alreadyinitialized', false); 215 | } 216 | 217 | function _updateSettings(newsettings) { 218 | settings = $.extend({}, settings, newsettings); 219 | 220 | // Update postfix and prefix texts if those settings were changed. 221 | if (newsettings.postfix) { 222 | var $postfix = originalinput.parent().find('.bootstrap-touchspin-postfix'); 223 | 224 | if ($postfix.length === 0) { 225 | _detached_postfix.insertAfter(originalinput); 226 | } 227 | 228 | originalinput.parent().find('.bootstrap-touchspin-postfix .input-group-text').text(newsettings.postfix); 229 | } 230 | 231 | if (newsettings.prefix) { 232 | var $prefix = originalinput.parent().find('.bootstrap-touchspin-prefix'); 233 | 234 | if ($prefix.length === 0) { 235 | _detached_prefix.insertBefore(originalinput); 236 | } 237 | 238 | originalinput.parent().find('.bootstrap-touchspin-prefix .input-group-text').text(newsettings.prefix); 239 | } 240 | 241 | _hideEmptyPrefixPostfix(); 242 | } 243 | 244 | function _buildHtml() { 245 | var initval = originalinput.val(), 246 | parentelement = originalinput.parent(); 247 | 248 | if (initval !== '') { 249 | // initval may not be parsable as a number (callback_after_calculation() may decorate it so it cant be parsed). Use the callbacks if provided. 250 | initval = settings.callback_before_calculation(initval); 251 | initval = settings.callback_after_calculation(parseFloat(initval).toFixed(settings.decimals)); 252 | } 253 | 254 | originalinput.data('initvalue', initval).val(initval); 255 | originalinput.addClass('form-control'); 256 | 257 | verticalbuttons_html = ` 258 | 259 | 260 | 261 | 262 | 263 | 264 | `; 265 | 266 | if (parentelement.hasClass('input-group')) { 267 | _advanceInputGroup(parentelement); 268 | } else { 269 | _buildInputGroup(); 270 | } 271 | } 272 | 273 | function _advanceInputGroup(parentelement) { 274 | parentelement.addClass('bootstrap-touchspin'); 275 | 276 | var prev = originalinput.prev(), 277 | next = originalinput.next(); 278 | 279 | var downhtml, 280 | uphtml, 281 | prefixhtml = ` 282 | 283 | ${settings.prefix} 284 | 285 | `, 286 | postfixhtml = ` 287 | 288 | ${settings.postfix} 289 | 290 | `; 291 | 292 | if (settings.verticalbuttons) { 293 | $(verticalbuttons_html).insertAfter(originalinput); 294 | } 295 | else { 296 | if (prev.hasClass('input-group-btn') || prev.hasClass('input-group-prepend')) { 297 | downhtml = ` 298 | 299 | `; 300 | prev.append(downhtml); 301 | } else { 302 | downhtml = ` 303 | 304 | 305 | 306 | `; 307 | $(downhtml).insertBefore(originalinput); 308 | } 309 | 310 | if (next.hasClass('input-group-btn') || next.hasClass('input-group-append')) { 311 | uphtml = ` 312 | 313 | `; 314 | next.prepend(uphtml); 315 | } else { 316 | uphtml = ` 317 | 318 | 319 | 320 | `; 321 | $(uphtml).insertAfter(originalinput); 322 | } 323 | } 324 | 325 | $(prefixhtml).insertBefore(originalinput); 326 | $(postfixhtml).insertAfter(originalinput); 327 | 328 | container = parentelement; 329 | } 330 | 331 | function _buildInputGroup() { 332 | var html; 333 | 334 | var inputGroupSize = ''; 335 | if (originalinput.hasClass('input-sm') || originalinput.hasClass('form-control-sm')) { 336 | inputGroupSize = 'input-group-sm'; 337 | } else if (originalinput.hasClass('input-lg') || originalinput.hasClass('form-control-lg')) { 338 | inputGroupSize = 'input-group-lg'; 339 | } 340 | 341 | if (settings.verticalbuttons) { 342 | html = ` 343 |
344 | 345 | ${settings.prefix} 346 | 347 | 348 | ${settings.postfix} 349 | 350 | ${verticalbuttons_html} 351 |
352 | `; 353 | } else { 354 | html = ` 355 |
356 | 357 | 358 | 359 | 360 | ${settings.prefix} 361 | 362 | 363 | ${settings.postfix} 364 | 365 | 366 | 367 | 368 |
`; 369 | } 370 | 371 | container = $(html).insertBefore(originalinput); 372 | 373 | $('.bootstrap-touchspin-prefix', container).after(originalinput); 374 | 375 | if (originalinput.hasClass('input-sm') || originalinput.hasClass('form-control-sm')) { 376 | container.addClass('input-group-sm'); 377 | } else if (originalinput.hasClass('input-lg') || originalinput.hasClass('form-control-lg')) { 378 | container.addClass('input-group-lg'); 379 | } 380 | } 381 | 382 | function _initElements() { 383 | elements = { 384 | down: $('.bootstrap-touchspin-down', container), 385 | up: $('.bootstrap-touchspin-up', container), 386 | input: $('input', container), 387 | prefix: $('.bootstrap-touchspin-prefix', container).addClass(settings.prefix_extraclass), 388 | postfix: $('.bootstrap-touchspin-postfix', container).addClass(settings.postfix_extraclass) 389 | }; 390 | } 391 | 392 | function _hideEmptyPrefixPostfix() { 393 | if (settings.prefix === '') { 394 | _detached_prefix = elements.prefix.detach(); 395 | } 396 | 397 | if (settings.postfix === '') { 398 | _detached_postfix = elements.postfix.detach(); 399 | } 400 | } 401 | 402 | function _bindEvents() { 403 | originalinput.on('keydown.touchspin', function (ev) { 404 | var code = ev.keyCode || ev.which; 405 | 406 | if (code === 38) { 407 | if (spinning !== 'up') { 408 | upOnce(); 409 | startUpSpin(); 410 | } 411 | ev.preventDefault(); 412 | } else if (code === 40) { 413 | if (spinning !== 'down') { 414 | downOnce(); 415 | startDownSpin(); 416 | } 417 | ev.preventDefault(); 418 | } else if (code === 9 || code === 13) { 419 | _checkValue(); 420 | } 421 | }); 422 | 423 | originalinput.on('keyup.touchspin', function (ev) { 424 | var code = ev.keyCode || ev.which; 425 | 426 | if (code === 38) { 427 | stopSpin(); 428 | } else if (code === 40) { 429 | stopSpin(); 430 | } 431 | }); 432 | 433 | // change is fired before blur, so we need to work around that 434 | $(document).on('mousedown touchstart', function(event) { 435 | if ($(event.target).is(originalinput)) { 436 | return; 437 | } 438 | 439 | _checkValue(); 440 | }); 441 | 442 | originalinput.on('blur.touchspin', function () { 443 | _checkValue(); 444 | }); 445 | 446 | elements.down.on('keydown', function (ev) { 447 | var code = ev.keyCode || ev.which; 448 | 449 | if (code === 32 || code === 13) { 450 | if (spinning !== 'down') { 451 | downOnce(); 452 | startDownSpin(); 453 | } 454 | ev.preventDefault(); 455 | } 456 | }); 457 | 458 | elements.down.on('keyup.touchspin', function (ev) { 459 | var code = ev.keyCode || ev.which; 460 | 461 | if (code === 32 || code === 13) { 462 | stopSpin(); 463 | } 464 | }); 465 | 466 | elements.up.on('keydown.touchspin', function (ev) { 467 | var code = ev.keyCode || ev.which; 468 | 469 | if (code === 32 || code === 13) { 470 | if (spinning !== 'up') { 471 | upOnce(); 472 | startUpSpin(); 473 | } 474 | ev.preventDefault(); 475 | } 476 | }); 477 | 478 | elements.up.on('keyup.touchspin', function (ev) { 479 | var code = ev.keyCode || ev.which; 480 | 481 | if (code === 32 || code === 13) { 482 | stopSpin(); 483 | } 484 | }); 485 | 486 | elements.down.on('mousedown.touchspin', function (ev) { 487 | elements.down.off('touchstart.touchspin'); // android 4 workaround 488 | 489 | if (originalinput.is(':disabled,[readonly]')) { 490 | return; 491 | } 492 | 493 | downOnce(); 494 | startDownSpin(); 495 | 496 | ev.preventDefault(); 497 | ev.stopPropagation(); 498 | }); 499 | 500 | elements.down.on('touchstart.touchspin', function (ev) { 501 | elements.down.off('mousedown.touchspin'); // android 4 workaround 502 | 503 | if (originalinput.is(':disabled,[readonly]')) { 504 | return; 505 | } 506 | 507 | downOnce(); 508 | startDownSpin(); 509 | 510 | ev.preventDefault(); 511 | ev.stopPropagation(); 512 | }); 513 | 514 | elements.up.on('mousedown.touchspin', function (ev) { 515 | elements.up.off('touchstart.touchspin'); // android 4 workaround 516 | 517 | if (originalinput.is(':disabled,[readonly]')) { 518 | return; 519 | } 520 | 521 | upOnce(); 522 | startUpSpin(); 523 | 524 | ev.preventDefault(); 525 | ev.stopPropagation(); 526 | }); 527 | 528 | elements.up.on('touchstart.touchspin', function (ev) { 529 | elements.up.off('mousedown.touchspin'); // android 4 workaround 530 | 531 | if (originalinput.is(':disabled,[readonly]')) { 532 | return; 533 | } 534 | 535 | upOnce(); 536 | startUpSpin(); 537 | 538 | ev.preventDefault(); 539 | ev.stopPropagation(); 540 | }); 541 | 542 | elements.up.on('mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin', function (ev) { 543 | if (!spinning) { 544 | return; 545 | } 546 | 547 | ev.stopPropagation(); 548 | stopSpin(); 549 | }); 550 | 551 | elements.down.on('mouseup.touchspin mouseout.touchspin touchleave.touchspin touchend.touchspin touchcancel.touchspin', function (ev) { 552 | if (!spinning) { 553 | return; 554 | } 555 | 556 | ev.stopPropagation(); 557 | stopSpin(); 558 | }); 559 | 560 | elements.down.on('mousemove.touchspin touchmove.touchspin', function (ev) { 561 | if (!spinning) { 562 | return; 563 | } 564 | 565 | ev.stopPropagation(); 566 | ev.preventDefault(); 567 | }); 568 | 569 | elements.up.on('mousemove.touchspin touchmove.touchspin', function (ev) { 570 | if (!spinning) { 571 | return; 572 | } 573 | 574 | ev.stopPropagation(); 575 | ev.preventDefault(); 576 | }); 577 | 578 | originalinput.on('mousewheel.touchspin DOMMouseScroll.touchspin', function (ev) { 579 | if (!settings.mousewheel || !originalinput.is(':focus')) { 580 | return; 581 | } 582 | 583 | var delta = ev.originalEvent.wheelDelta || -ev.originalEvent.deltaY || -ev.originalEvent.detail; 584 | 585 | ev.stopPropagation(); 586 | ev.preventDefault(); 587 | 588 | if (delta < 0) { 589 | downOnce(); 590 | } else { 591 | upOnce(); 592 | } 593 | }); 594 | } 595 | 596 | function _bindEventsInterface() { 597 | originalinput.on('touchspin.destroy', function () { 598 | _destroy(); 599 | }); 600 | 601 | originalinput.on('touchspin.uponce', function () { 602 | stopSpin(); 603 | upOnce(); 604 | }); 605 | 606 | originalinput.on('touchspin.downonce', function () { 607 | stopSpin(); 608 | downOnce(); 609 | }); 610 | 611 | originalinput.on('touchspin.startupspin', function () { 612 | startUpSpin(); 613 | }); 614 | 615 | originalinput.on('touchspin.startdownspin', function () { 616 | startDownSpin(); 617 | }); 618 | 619 | originalinput.on('touchspin.stopspin', function () { 620 | stopSpin(); 621 | }); 622 | 623 | originalinput.on('touchspin.updatesettings', function (e, newsettings) { 624 | changeSettings(newsettings); 625 | }); 626 | } 627 | 628 | function _setupMutationObservers() { 629 | if (typeof MutationObserver !== 'undefined') { 630 | // MutationObserver is available 631 | const observer = new MutationObserver((mutations) => { 632 | mutations.forEach((mutation) => { 633 | if (mutation.type === 'attributes' && (mutation.attributeName === 'disabled' || mutation.attributeName === 'readonly')) { 634 | _updateButtonDisabledState(); 635 | } 636 | }); 637 | }); 638 | 639 | observer.observe(originalinput[0], {attributes: true}); 640 | } 641 | } 642 | 643 | function _forcestepdivisibility(value) { 644 | switch (settings.forcestepdivisibility) { 645 | case 'round': 646 | return (Math.round(value / settings.step) * settings.step).toFixed(settings.decimals); 647 | case 'floor': 648 | return (Math.floor(value / settings.step) * settings.step).toFixed(settings.decimals); 649 | case 'ceil': 650 | return (Math.ceil(value / settings.step) * settings.step).toFixed(settings.decimals); 651 | default: 652 | return value.toFixed(settings.decimals); 653 | } 654 | } 655 | 656 | function _checkValue() { 657 | var val, parsedval, returnval; 658 | 659 | val = settings.callback_before_calculation(originalinput.val()); 660 | 661 | if (val === '') { 662 | if (settings.replacementval !== '') { 663 | originalinput.val(settings.replacementval); 664 | originalinput.trigger('change'); 665 | } 666 | return; 667 | } 668 | 669 | if (settings.decimals > 0 && val === '.') { 670 | return; 671 | } 672 | 673 | parsedval = parseFloat(val); 674 | 675 | if (isNaN(parsedval)) { 676 | if (settings.replacementval !== '') { 677 | parsedval = settings.replacementval; 678 | } else { 679 | parsedval = 0; 680 | } 681 | } 682 | 683 | returnval = parsedval; 684 | 685 | if (parsedval.toString() !== val) { 686 | returnval = parsedval; 687 | } 688 | 689 | returnval = _forcestepdivisibility(parsedval); 690 | 691 | if ((settings.min !== null) && (parsedval < settings.min)) { 692 | returnval = settings.min; 693 | } 694 | 695 | if ((settings.max !== null) && (parsedval > settings.max)) { 696 | returnval = settings.max; 697 | } 698 | 699 | if (parseFloat(parsedval).toString() !== parseFloat(returnval).toString()) { 700 | originalinput.val(returnval); 701 | } 702 | 703 | originalinput.val(settings.callback_after_calculation(parseFloat(returnval).toFixed(settings.decimals))); 704 | } 705 | 706 | function _getBoostedStep() { 707 | if (!settings.booster) { 708 | return settings.step; 709 | } else { 710 | var boosted = Math.pow(2, Math.floor(spincount / settings.boostat)) * settings.step; 711 | 712 | if (settings.maxboostedstep) { 713 | if (boosted > settings.maxboostedstep) { 714 | boosted = settings.maxboostedstep; 715 | value = Math.round((value / boosted)) * boosted; 716 | } 717 | } 718 | 719 | return Math.max(settings.step, boosted); 720 | } 721 | } 722 | 723 | function valueIfIsNaN() { 724 | if (typeof (settings.firstclickvalueifempty) === 'number') { 725 | return settings.firstclickvalueifempty; 726 | } else { 727 | return (settings.min + settings.max) / 2; 728 | } 729 | } 730 | 731 | function _updateButtonDisabledState() { 732 | const isDisabled = originalinput.is(':disabled,[readonly]'); 733 | elements.up.prop('disabled', isDisabled); 734 | elements.down.prop('disabled', isDisabled); 735 | 736 | if (isDisabled) { 737 | stopSpin(); 738 | } 739 | } 740 | 741 | function upOnce() { 742 | if (originalinput.is(':disabled,[readonly]')) { 743 | return; 744 | } 745 | 746 | _checkValue(); 747 | 748 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 749 | 750 | var initvalue = value; 751 | var boostedstep; 752 | 753 | if (isNaN(value)) { 754 | value = valueIfIsNaN(); 755 | } else { 756 | boostedstep = _getBoostedStep(); 757 | value = value + boostedstep; 758 | } 759 | 760 | if ((settings.max !== null) && (value >= settings.max)) { 761 | value = settings.max; 762 | originalinput.trigger('touchspin.on.max'); 763 | stopSpin(); 764 | } 765 | 766 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 767 | 768 | if (initvalue !== value) { 769 | originalinput.trigger('change'); 770 | } 771 | } 772 | 773 | function downOnce() { 774 | if (originalinput.is(':disabled,[readonly]')) { 775 | return; 776 | } 777 | 778 | _checkValue(); 779 | 780 | value = parseFloat(settings.callback_before_calculation(elements.input.val())); 781 | 782 | var initvalue = value; 783 | var boostedstep; 784 | 785 | if (isNaN(value)) { 786 | value = valueIfIsNaN(); 787 | } else { 788 | boostedstep = _getBoostedStep(); 789 | value = value - boostedstep; 790 | } 791 | 792 | if ((settings.min !== null) && (value <= settings.min)) { 793 | value = settings.min; 794 | originalinput.trigger('touchspin.on.min'); 795 | stopSpin(); 796 | } 797 | 798 | elements.input.val(settings.callback_after_calculation(parseFloat(value).toFixed(settings.decimals))); 799 | 800 | if (initvalue !== value) { 801 | originalinput.trigger('change'); 802 | } 803 | } 804 | 805 | function startDownSpin() { 806 | if (originalinput.is(':disabled,[readonly]')) { 807 | return; 808 | } 809 | 810 | stopSpin(); 811 | 812 | spincount = 0; 813 | spinning = 'down'; 814 | 815 | originalinput.trigger('touchspin.on.startspin'); 816 | originalinput.trigger('touchspin.on.startdownspin'); 817 | 818 | downDelayTimeout = setTimeout(function () { 819 | downSpinTimer = setInterval(function () { 820 | spincount++; 821 | downOnce(); 822 | }, settings.stepinterval); 823 | }, settings.stepintervaldelay); 824 | } 825 | 826 | function startUpSpin() { 827 | if (originalinput.is(':disabled,[readonly]')) { 828 | return; 829 | } 830 | 831 | stopSpin(); 832 | 833 | spincount = 0; 834 | spinning = 'up'; 835 | 836 | originalinput.trigger('touchspin.on.startspin'); 837 | originalinput.trigger('touchspin.on.startupspin'); 838 | 839 | upDelayTimeout = setTimeout(function () { 840 | upSpinTimer = setInterval(function () { 841 | spincount++; 842 | upOnce(); 843 | }, settings.stepinterval); 844 | }, settings.stepintervaldelay); 845 | } 846 | 847 | function stopSpin() { 848 | clearTimeout(downDelayTimeout); 849 | clearTimeout(upDelayTimeout); 850 | clearInterval(downSpinTimer); 851 | clearInterval(upSpinTimer); 852 | 853 | switch (spinning) { 854 | case 'up': 855 | originalinput.trigger('touchspin.on.stopupspin'); 856 | originalinput.trigger('touchspin.on.stopspin'); 857 | break; 858 | case 'down': 859 | originalinput.trigger('touchspin.on.stopdownspin'); 860 | originalinput.trigger('touchspin.on.stopspin'); 861 | break; 862 | } 863 | 864 | spincount = 0; 865 | spinning = false; 866 | } 867 | 868 | }); 869 | 870 | }; 871 | 872 | })); 873 | 874 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true 7 | }, 8 | "include": [ 9 | "./src/**/*.ts", 10 | ] 11 | } 12 | --------------------------------------------------------------------------------