├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── deploy.sh ├── jest.config.js ├── package-lock.json ├── package.json ├── postbuild.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── components │ ├── VNumeric │ │ ├── VCalculator.ts │ │ ├── VNumeric.ts │ │ └── VNumericInput.ts │ └── index.ts ├── main.ts ├── plugins │ └── vuetify.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts └── shims-vuetify.ts ├── testcdn └── index.html ├── tests └── unit │ ├── VNumeric.spec.ts │ └── __snapshots__ │ └── VNumeric.spec.ts.snap ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 17 | }, 18 | overrides: [ 19 | { 20 | files: [ 21 | '**/__tests__/*.{j,t}s?(x)', 22 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 23 | ], 24 | env: { 25 | jest: true 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run lib 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | script: 5 | - npm run lint 6 | - npm run build 7 | - npm run test:unit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aleksandr Kolesnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuetify-numeric 2 | Numeric input components for use with [vuetifyjs](https://vuetifyjs.com). 3 | 4 |

5 | 6 | Travis (.org) branch 7 | 8 | 9 | npm 10 | 11 | 12 | npm 13 | 14 |

15 | 16 | ## Features 17 | - Built-in calculator 18 | - Smart numeric input 19 | - Locale support number format 20 | - Adjustable text color 21 | - Groupping digits 22 | - Right number alignement 23 | - Show prefix (currency ...) near your number 24 | - No thirdpatry solutions is used 25 | - Vuetify [VTextField](https://vuetifyjs.com/en/components/text-fields) compatible 26 | 27 | ## Keyboard shortcuts 28 | | Key | Action | 29 | | ---- | -------- | 30 | | Enter | Activate calculator or calculate your expression and close the calculator. (Note) You can change calculator's activation key | 31 | | Delete | Reset calculator | 32 | | . or , | Swich your input between integer and fraction part of number | 33 | | - | Change your input number sign | 34 | 35 | ## Demo & Playground 36 | See [Live demo ](https://kolesnikovav.github.io/vuetify-numeric/). or Codesandbox example [codesandbox](https://codesandbox.io/s/condescending-mendel-5zpqn) 37 | 38 | ## The v-numeric component 39 | The component extends the Vuetify `v-text-field` component. 40 | 41 | ## How to use 42 | 43 | Install the package: 44 | ``` 45 | yarn add vuetify-numeric 46 | ``` 47 | 48 | Add the package to your app entry point: 49 | ``` 50 | import VNumeric from 'vuetify-numeric/vuetify-numeric.umd.min' 51 | ``` 52 | 53 | Or (in develop case) 54 | ``` 55 | import VNumeric from 'vuetify-numeric/vuetify-numeric.umd' 56 | ``` 57 | Than, register this plugin 58 | ``` 59 | Vue.use(VNumeric) 60 | ``` 61 | Once the plugin has been installed, you can now use the `v-numeric` component in your templates. 62 | Use `v-model` to bind to the value. 63 | ``` 64 | 67 | 68 | 77 | ``` 78 | 79 | ### Props: 80 | 81 | | Prop | description | type | default | 82 | | ---- | ---- | ------- | --- | 83 | | min | Sets minimum value | Number | - Number.MAX (infinity) | 84 | | max | Sets maximum value | Number | Number.MAX (infinity)| 85 | | length | Sets maximum number of digits | Number | 10 | 86 | | precision | Number of digits after decimal point | Number | 0 | 87 | | negativeTextColor | Text color when number is negative | String | red | 88 | | locale | Current locale | String | en-US | 89 | | useGrouping | use grouping digits | Boolean | true | 90 | | elevation | Sets the calculator elevation | Number | 10 | 91 | | fab | FAB-kind calculator's button | Boolean | false | 92 | | text | use transparent background in calculator | Boolean | false | 93 | | calcStyle | You can customize calculator's button style separately from input field. This is not mandatory.| object | undefined | 94 | | calcIcon | You can customize calculator's icon. If it's undefined, the calculator icon does not appear.| string | 'mdi-calculator' | 95 | | useCalculator | Turn on/off calculator usage.| boolean | true | 96 | | openKey | Key for open build-in calculator | String |'Enter' 97 | | calcNoTabindex | Set or not tabindex attribute in calc icon | Boolean | false | 98 | 99 | ### calcStyle object properties: 100 | This object uses for customizing calculator buttons, and consist of the same Vuetify v-btn properies. 101 | For details, see [official documentation](https://vuetifyjs.com/en/components/buttons/#api) 102 | 103 | calcStyle: { 104 | fab: false, 105 | outlined: false, 106 | rounded: false, 107 | text: false, 108 | tile: false, 109 | large: false, 110 | small: false 111 | } 112 | 113 | Anover props are derived from [v-text-field](https://vuetifyjs.com/en/components/text-fields) component 114 | 115 | ### Events: 116 | 117 | `@input`: Emitted when value is changed after user input. 118 | `@change`: Emitted formatted value as string when that is changed after user input. 119 | 120 | ### CDN example: 121 | 122 | You can use this library without installation, via cdn provider 123 | ``` 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
147 | 148 | 149 | 150 | 151 | 157 | 158 | 159 | 160 | ``` 161 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | npm run build 6 | 7 | cd dist 8 | 9 | git init 10 | git add -A 11 | git commit -m 'deploy' 12 | 13 | git push -f git@github.com:kolesnikovav/vuetify-numeric.git master:gh-pages 14 | 15 | cd - -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | transformIgnorePatterns: ['/node_modules/(?!vuetify)'] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuetify-numeric", 3 | "version": "0.2.1", 4 | "private": false, 5 | "description": "Numeric input components for use with vuetifyjs", 6 | "author": { 7 | "name": "Aleksandr Kolesnikov", 8 | "email": "alex.v.kolesnikov72@gmail.com" 9 | }, 10 | "scripts": { 11 | "serve": "vue-cli-service serve", 12 | "build": "vue-cli-service build", 13 | "test:unit": "vue-cli-service test:unit", 14 | "lint": "vue-cli-service lint", 15 | "lib": "vue-cli-service build --target lib 'src/components/index.ts'", 16 | "publish": "npm run lib && node postbuild.js" 17 | }, 18 | "main": "vuetify-numeric.umd.min.js", 19 | "unpkg": "vuetify-numeric.umd.min.js", 20 | "dependencies": { 21 | "core-js": "^3.6.4", 22 | "vue": "^2.6.14", 23 | "vuetify": "^2.6.3" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "^25.1.4", 27 | "@typescript-eslint/eslint-plugin": "^2.23.0", 28 | "@typescript-eslint/parser": "^2.23.0", 29 | "@vue/cli-plugin-babel": "~4.5.15", 30 | "@vue/cli-plugin-eslint": "~4.5.15", 31 | "@vue/cli-plugin-typescript": "~4.5.15", 32 | "@vue/cli-plugin-unit-jest": "~4.5.15", 33 | "@vue/cli-service": "~4.5.15", 34 | "@vue/eslint-config-standard": "^5.1.2", 35 | "@vue/eslint-config-typescript": "^5.0.2", 36 | "@vue/test-utils": "1.0.0-beta.32", 37 | "eslint": "^6.8.0", 38 | "eslint-plugin-import": "^2.20.1", 39 | "eslint-plugin-node": "^11.0.0", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.1", 42 | "eslint-plugin-vue": "^6.2.2", 43 | "node-sass": "^4.12.0", 44 | "sass": "^1.19.0", 45 | "sass-loader": "^8.0.2", 46 | "typescript": "~4.1.5", 47 | "vue-cli-plugin-vuetify": "~2.4.5", 48 | "vue-template-compiler": "^2.6.14", 49 | "vuetify-loader": "^1.7.3" 50 | }, 51 | "homepage": "https://github.com/kolesnikovav/vuetify-numeric", 52 | "jsdelivr": "vuetify-numeric.umd.min.js", 53 | "keywords": [ 54 | "vuetify", 55 | "vue", 56 | "calculator", 57 | "numeric input" 58 | ], 59 | "license": "MIT", 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/kolesnikovav/vuetify-numeric.git" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postbuild.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const fs = require('fs') 3 | 4 | const DIST_LIB_PATH = 'dist/' 5 | const README_PATH = 'README.md' 6 | const PACKJS_PATH = 'package.json' 7 | 8 | const PATH_TO = DIST_LIB_PATH + README_PATH 9 | const PATH_TO_JSON = 'dist/package.json' 10 | 11 | function copyFilesIntoDistFolder () { 12 | if (!fs.existsSync(README_PATH) || !fs.existsSync(PACKJS_PATH)) { 13 | throw new Error('README.md or package.json does not exist') 14 | } else { 15 | fs.copyFileSync(README_PATH, PATH_TO) 16 | fs.copyFileSync(PACKJS_PATH, PATH_TO_JSON) 17 | } 18 | } 19 | 20 | copyFilesIntoDistFolder() 21 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kolesnikovav/vuetify-numeric/bfa44944cafd33580770588b95616b01584bc439/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 203 | 204 | 250 | 251 | 259 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kolesnikovav/vuetify-numeric/bfa44944cafd33580770588b95616b01584bc439/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/components/VNumeric/VCalculator.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | import { VTextFieldA, VBtnA, VRowA, VSheetA } from '../../shims-vuetify' 3 | 4 | type operationType = ((a: number|string, b: number| string) => number)|undefined 5 | 6 | export default Vue.extend({ 7 | name: 'v-calculator', 8 | props: { 9 | isActive: { 10 | type: Boolean, 11 | default: false 12 | }, 13 | elevation: { 14 | type: Number, 15 | default: 0 16 | }, 17 | dark: { 18 | type: Boolean, 19 | default: false 20 | }, 21 | fab: { 22 | type: Boolean, 23 | default: false 24 | }, 25 | outlined: { 26 | type: Boolean, 27 | default: false 28 | }, 29 | rounded: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | text: { 34 | type: Boolean, 35 | default: false 36 | }, 37 | useGrouping: { 38 | type: Boolean, 39 | default: true 40 | }, 41 | locale: { 42 | type: String, 43 | default: 'en-US' 44 | }, 45 | precision: { 46 | type: Number, 47 | default: 0 48 | }, 49 | initialValue: { 50 | type: Number, 51 | default: 0 52 | }, 53 | negativeTextColor: { 54 | type: String, 55 | default: 'red' 56 | }, 57 | calcStyle: { 58 | type: Object, 59 | default: undefined 60 | } 61 | }, 62 | computed: { 63 | numberFormatter (): Intl.NumberFormat { 64 | return new Intl.NumberFormat(this.locale, { 65 | useGrouping: this.useGrouping 66 | }) 67 | }, 68 | resultNumber (): string { 69 | return this.numberFormatter.format(Number(this.value)) 70 | }, 71 | computedColor (): string| undefined { 72 | if (Number(this.$data.value) < 0 && this.negativeTextColor) { 73 | return this.negativeTextColor 74 | } else return undefined 75 | }, 76 | computedOutlined (): boolean { 77 | if (!this.calcStyle) return this.outlined 78 | if (this.calcStyle.outlined === undefined) return this.outlined 79 | return this.calcStyle.outlined 80 | }, 81 | computedRounded (): boolean { 82 | if (!this.calcStyle) return this.rounded 83 | if (this.calcStyle.rounded === undefined) return this.rounded 84 | return this.calcStyle.rounded 85 | }, 86 | computedText (): boolean { 87 | if (!this.calcStyle) return this.text 88 | if (this.calcStyle.text === undefined) return this.text 89 | return this.calcStyle.text 90 | }, 91 | computedTile (): boolean { 92 | if (!this.calcStyle) return false 93 | return (this.calcStyle.tile === undefined) ? false : this.calcStyle.tile 94 | }, 95 | computedLarge (): boolean { 96 | if (!this.calcStyle) return false 97 | return (this.calcStyle.large === undefined) ? false : this.calcStyle.large 98 | }, 99 | computedSmall (): boolean { 100 | if (!this.calcStyle) return false 101 | return (this.calcStyle.small === undefined) ? false : this.calcStyle.small 102 | }, 103 | computedWidth (): string { 104 | if (!this.calcStyle) return '288px' 105 | return (this.calcStyle.width === undefined) ? '288px' : this.calcStyle.width 106 | }, 107 | computedHeight (): string { 108 | if (!this.calcStyle) return '246px' 109 | return (this.calcStyle.height === undefined) 110 | ? this.calcStyle.small ? '212px' : '246px' 111 | : this.calcStyle.height 112 | }, 113 | textOperand (): string { 114 | if (!this.operand) return '' 115 | return (this.operand === 0) ? '' : this.operand.toString() 116 | } 117 | }, 118 | data: () => ({ 119 | value: '0', 120 | operand: 0, 121 | operation: undefined as operationType 122 | }), 123 | watch: { 124 | initialValue: { 125 | immediate: true, 126 | deep: true, 127 | handler (newVal) { 128 | if (newVal) { 129 | this.value = newVal.toString() 130 | } 131 | } 132 | }, 133 | computedColor (newVal) { 134 | const input = this.genResultInput() 135 | if (input) { 136 | input.style.color = newVal || null 137 | } 138 | } 139 | }, 140 | methods: { 141 | reset (): void { 142 | this.value = '0' 143 | this.operation = undefined 144 | this.operand = 0 145 | }, 146 | genResultInput (): HTMLInputElement| undefined { 147 | const inputs = (this.$refs.calcResult as Vue).$el.getElementsByTagName('input') 148 | if (inputs && inputs.length > 0) { 149 | return inputs[0] 150 | } 151 | }, 152 | getOperation (simbol: string): operationType { 153 | if (simbol === '+') return (a: number|string, b: number|string) => { return Number(a) + Number(b) } 154 | else if (simbol === '-') return (a: number|string, b: number|string) => { return Number(a) - Number(b) } 155 | else if (simbol === '*') return (a: number|string, b: number|string) => { return Number(a) * Number(b) } 156 | else if (simbol === '÷' || simbol === '/') return (a: number|string, b: number|string) => { return Number(a) / Number(b) } 157 | else if (simbol === '%') return (a: number|string, b: number|string) => { return (Number(a) / 100) * Number(b) } 158 | }, 159 | changeValue (newVal: KeyboardEvent| string) { 160 | if (!this.isActive) return 161 | let v: string 162 | if (newVal instanceof KeyboardEvent) { 163 | v = newVal.key 164 | } else { 165 | v = newVal 166 | } 167 | if (['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '00'].includes(v)) { 168 | if (this.value === '0') this.value = v 169 | else this.value += v 170 | } else if (v === 'Backspace' || v === '←') { 171 | if (this.value.length === 2 && this.value.startsWith('-')) { 172 | this.value = '0' 173 | } else { 174 | this.value = this.value.length <= 1 ? '0' : this.value.substring(0, this.value.length - 1) 175 | } 176 | } else if (v.toUpperCase() === 'C') { 177 | this.reset() 178 | } else if (v === ',' || v === '.') { 179 | if (this.value.indexOf('.') === -1) { 180 | this.value += '.' 181 | } 182 | } else if (v === '±') { 183 | if (this.value.toString().startsWith('-')) this.value = this.value.toString().substring(1, this.value.length) 184 | else this.value = '-' + this.value 185 | } else if (v === '1/x') { 186 | if (this.value !== '0') this.value = (1 / Number.parseFloat(this.value)).toString() 187 | } else if (['+', '-', '*', '÷', '/', '%'].includes(v)) { 188 | this.calculate() 189 | this.operation = this.getOperation(v) 190 | this.operand = Number(this.value) 191 | this.value = '0' 192 | } else if (['=', 'Enter', 'OK'].includes(v)) { 193 | this.calculate() 194 | this.operation = undefined 195 | this.operand = 0 196 | if (v === 'Enter' || v === 'OK') this.returnValue() 197 | } else if (v === 'CE') { 198 | this.value = '0' 199 | } else if (v === 'Escape') { 200 | this.reset() 201 | this.$emit('return-value', undefined) 202 | } else if (v === 'Delete') { 203 | this.value = '0' 204 | this.$emit('return-value', this.value) 205 | this.reset() 206 | } 207 | }, 208 | returnValue (): void { 209 | this.$emit('return-value', this.value) 210 | this.reset() 211 | }, 212 | calculate (): void { 213 | if (this.value && this.operand && this.operation) { 214 | const res = this.operation(this.operand, this.value) 215 | this.value = res.toString() 216 | } 217 | }, 218 | genNumberButton (numberValue: string): VNode { 219 | return this.$createElement(VBtnA, { 220 | style: { 221 | 'padding-left': '0px', 222 | 'padding-right': '0px', 223 | 'max-width': '48px', 224 | 'min-width': '48px' 225 | }, 226 | props: { 227 | fab: (this.calcStyle && this.calcStyle.fab) ? this.calcStyle.fab : this.fab, 228 | outlined: this.computedOutlined, 229 | rounded: this.computedRounded, 230 | text: this.computedText, 231 | tile: this.computedTile, 232 | large: this.computedLarge, 233 | small: this.computedSmall 234 | }, 235 | domProps: { 236 | innerHTML: numberValue 237 | }, 238 | on: { 239 | click: () => this.changeValue(numberValue) 240 | } 241 | }) 242 | }, 243 | genActionsButton (actValue: string): VNode { 244 | return this.$createElement(VBtnA, { 245 | style: { 246 | 'padding-left': '0px', 247 | 'padding-right': '0px', 248 | 'max-width': '48px', 249 | 'min-width': '48px' 250 | }, 251 | props: { 252 | fab: (this.calcStyle && this.calcStyle.fab) ? this.calcStyle.fab : this.fab, 253 | outlined: this.computedOutlined, 254 | rounded: this.computedRounded, 255 | text: this.computedText, 256 | tile: this.computedTile, 257 | large: this.computedLarge, 258 | small: this.computedSmall 259 | }, 260 | domProps: { 261 | innerHTML: actValue 262 | }, 263 | on: { 264 | click: () => this.changeValue(actValue) 265 | } 266 | }) 267 | }, 268 | genRow (content: string[]): VNode|VNode[] { 269 | const rowContent: VNode[] = [] 270 | // const actButtons = ['+', '±', 'C', '-', '%', 'CE', '*', '1/x', '←', '.', '÷', '=', 'OK'] 271 | const actButtons = ['+', '\u00B1', 'C', '-', '%', 'CE', '*', '1/x', '\u2190', '.', '\u00F7', '=', 'OK'] 272 | content.map(v => { 273 | if (actButtons.includes(v)) { 274 | rowContent.push(this.genActionsButton(v)) 275 | } else { 276 | rowContent.push(this.genNumberButton(v)) 277 | } 278 | }) 279 | return this.$createElement(VRowA, { 280 | style: { 281 | 'margin-left': '0px', 282 | 'margin-right': '0px' 283 | } 284 | }, rowContent) 285 | }, 286 | genResult (): VNode { 287 | return this.$createElement(VTextFieldA, { 288 | ref: 'calcResult', 289 | props: { 290 | outlined: true, 291 | reverse: true, 292 | readonly: true, 293 | value: this.resultNumber, 294 | autofocus: true, 295 | hint: this.textOperand, 296 | persistentHint: true 297 | }, 298 | style: { 299 | padding: '12px', 300 | 'font-size': '24px' 301 | } 302 | }) 303 | } 304 | }, 305 | mounted () { 306 | document.addEventListener('keydown', this.changeValue) 307 | }, 308 | beforeDestroy () { 309 | document.removeEventListener('keydown', this.changeValue) 310 | }, 311 | render (): VNode { 312 | const layer1 = this.genRow(['7', '8', '9', '+', '\u00B1', 'C']) 313 | const layer2 = this.genRow(['4', '5', '6', '-', '%', 'CE']) 314 | const layer3 = this.genRow(['1', '2', '3', '*', '1/x', '\u2190']) 315 | const layer4 = this.genRow(['0', '00', '.', '\u00F7', '=', 'OK']) 316 | const content = [] 317 | content.push(this.genResult()) 318 | content.push(layer1, layer2, layer3, layer4) 319 | return this.$createElement(VSheetA, { 320 | attrs: { 321 | tabindex: 0 322 | }, 323 | props: { 324 | width: this.computedWidth, 325 | height: this.computedHeight, 326 | elevation: this.elevation, 327 | dark: this.dark 328 | } 329 | }, content) 330 | } 331 | 332 | }) 333 | -------------------------------------------------------------------------------- /src/components/VNumeric/VNumeric.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | import { VMenuA, VTextFieldA } from '../../shims-vuetify' 3 | import VCalculator from './VCalculator' 4 | import VNumericInput from './VNumericInput' 5 | 6 | interface PosMenuType { 7 | bottom: number; 8 | right: number; 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | const VTextFieldProps = ((VTextFieldA as any).options as any).props 13 | 14 | export default Vue.extend({ 15 | name: 'v-numeric', 16 | props: { 17 | calcNoTabindex: { 18 | type: Boolean, 19 | default: false 20 | }, 21 | min: { 22 | type: Number, 23 | default: -Number.MAX_VALUE 24 | }, 25 | max: { 26 | type: Number, 27 | default: Number.MAX_VALUE 28 | }, 29 | length: { 30 | type: Number, 31 | default: 10 32 | }, 33 | precision: { 34 | type: [Number, String], 35 | default: 0 36 | }, 37 | negativeTextColor: { 38 | type: String, 39 | default: 'red' 40 | }, 41 | openKey: { 42 | type: String, 43 | default: 'Enter' 44 | }, 45 | textColor: { 46 | type: Function, 47 | default: undefined 48 | }, 49 | locale: { 50 | type: String, 51 | default: 'en-US' 52 | }, 53 | useGrouping: { 54 | type: Boolean, 55 | default: true 56 | }, 57 | /* customizing calculator */ 58 | elevation: { 59 | type: Number, 60 | default: 0 61 | }, 62 | fab: { 63 | type: Boolean, 64 | default: false 65 | }, 66 | rounded: { 67 | type: Boolean, 68 | default: false 69 | }, 70 | text: { 71 | type: Boolean, 72 | default: false 73 | }, 74 | calcIcon: { 75 | type: String, 76 | default: 'mdi-calculator' 77 | }, 78 | useCalculator: { 79 | type: Boolean, 80 | default: true 81 | }, 82 | calcStyle: { 83 | type: Object, 84 | default: undefined 85 | }, 86 | ...VTextFieldProps 87 | }, 88 | computed: { 89 | computedPrecision (): number { 90 | return Number(this.$props.precision) 91 | } 92 | }, 93 | data: () => ({ 94 | internalValue: 0, 95 | isMenuActive: false, 96 | xMenuPos: 0, 97 | yMenuPos: 0 98 | }), 99 | watch: { 100 | value: { 101 | deep: true, 102 | immediate: true, 103 | handler (newVal) { 104 | this.$data.internalValue = Number(newVal) 105 | } 106 | } 107 | }, 108 | methods: { 109 | activateCalculator () { 110 | this.isMenuActive = true 111 | }, 112 | closeCalculator (val: string|number|undefined) { 113 | this.isMenuActive = false 114 | this.changeValue(val) 115 | }, 116 | changeValue (val: string|number|undefined) { 117 | let result: number 118 | if (val) { 119 | if (this.computedPrecision > 0) { 120 | const p = Math.pow(10, this.computedPrecision) 121 | result = Math.round(Number(val) * p) / p 122 | } else { 123 | result = Math.round(Number(val)) 124 | } 125 | result = Math.max(Math.min(this.$props.max, result), this.$props.min) 126 | this.internalValue = result 127 | this.$emit('input', this.internalValue) 128 | } else if (val === 0) { 129 | this.internalValue = 0 130 | this.$emit('input', this.internalValue) 131 | } 132 | }, 133 | genCalculator (): VNode|undefined { 134 | if (!this.$props.useCalculator) return undefined 135 | return this.$createElement(VCalculator, { 136 | props: { 137 | initialValue: this.internalValue, 138 | locale: this.$props.locale, 139 | useGrouping: this.$props.useGrouping, 140 | negativeTextColor: this.$props.negativeTextColor, 141 | precision: this.computedPrecision, 142 | elevation: this.$props.elevation, 143 | fab: this.$props.fab, 144 | outlined: this.$props.outlined, 145 | rounded: this.$props.rounded, 146 | text: this.$props.text, 147 | dark: this.$props.dark, 148 | dense: this.$props.dense, 149 | isActive: this.isMenuActive, 150 | calcStyle: this.$props.calcStyle 151 | }, 152 | on: { 153 | 'return-value': (val: string|number|undefined) => this.closeCalculator(val) 154 | } 155 | }) 156 | }, 157 | setMenuPosition (rect: PosMenuType) { 158 | this.$data.yMenuPos = rect.bottom 159 | this.$data.xMenuPos = rect.right - 288 160 | }, 161 | genInput (): VNode { 162 | const props = Object.assign({}, this.$props) 163 | props.value = this.internalValue 164 | props.precision = this.computedPrecision 165 | return this.$createElement(VNumericInput, { 166 | domProps: { 167 | value: this.internalValue 168 | }, 169 | props, 170 | slot: 'activator', 171 | on: { 172 | 'activate-calculator': () => { 173 | this.activateCalculator() 174 | }, 175 | 'change-value': (val: string|number|undefined) => this.changeValue(val), 176 | 'resize-numeric-input': (rect: PosMenuType) => this.setMenuPosition(rect), 177 | input: (val: string|number) => { this.internalValue = Number(val) }, 178 | change: (val: string) => this.$emit('change', val) 179 | } 180 | }) 181 | }, 182 | computedWidth (): string { 183 | if (!this.$props.calcStyle) return '288px' 184 | return (this.$props.calcStyle.width === undefined) ? '288px' : this.$props.calcStyle.width 185 | }, 186 | computedHeight (): string { 187 | if (!this.$props.calcStyle) return '246px' 188 | return (this.$props.calcStyle.height === undefined) ? '246px' : this.$props.calcStyle.height 189 | } 190 | }, 191 | render (): VNode { 192 | // eslint-disable-next-line @typescript-eslint/no-this-alias 193 | const self = this 194 | return this.$createElement(VMenuA, { 195 | props: { 196 | absolute: true, 197 | positionX: this.xMenuPos, 198 | positionY: this.yMenuPos, 199 | closeOnContentClick: false, 200 | value: this.isMenuActive, 201 | dark: this.$props.dark, 202 | dense: this.$props.dense, 203 | width: this.computedWidth, 204 | maxWidth: this.computedWidth(), 205 | height: this.computedHeight, 206 | right: true 207 | }, 208 | scopedSlots: { 209 | 'activator' () { 210 | return self.genInput() 211 | } 212 | }, 213 | on: { 214 | 'update:return-value': (val: string|number|undefined) => this.closeCalculator(val) 215 | } 216 | }, [ 217 | this.genCalculator() 218 | ]) 219 | } 220 | }) 221 | -------------------------------------------------------------------------------- /src/components/VNumeric/VNumericInput.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | import { VIconA, VTextFieldA } from '../../shims-vuetify' 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const VTextFieldProps = ((VTextFieldA as any).options as any).props 6 | 7 | export default Vue.extend({ 8 | name: 'v-numeric-input', 9 | props: { 10 | calcNoTabindex: { 11 | type: Boolean, 12 | default: false 13 | }, 14 | min: { 15 | type: Number, 16 | default: -Number.MAX_VALUE 17 | }, 18 | max: { 19 | type: Number, 20 | default: Number.MAX_VALUE 21 | }, 22 | length: { 23 | type: Number, 24 | default: 10 25 | }, 26 | openKey: { 27 | type: String, 28 | default: 'Enter' 29 | }, 30 | precision: { 31 | type: Number, 32 | default: 0 33 | }, 34 | negativeTextColor: { 35 | type: String, 36 | default: 'red' 37 | }, 38 | textColor: { 39 | type: Function, 40 | default: undefined 41 | }, 42 | locale: { 43 | type: String, 44 | default: 'en-US' 45 | }, 46 | useGrouping: { 47 | type: Boolean, 48 | default: true 49 | }, 50 | calcIcon: { 51 | type: String, 52 | default: 'mdi-calculator' 53 | }, 54 | value: { 55 | type: [String, Number], 56 | default: 0 57 | }, 58 | ...VTextFieldProps 59 | }, 60 | data: () => ({ 61 | internalValue: 0, 62 | fractDigitsEdited: false, 63 | fractPart: '0', 64 | isFocused: false, 65 | clrValue: false 66 | }), 67 | computed: { 68 | numberFormatter (): Intl.NumberFormat { 69 | return new Intl.NumberFormat(this.$props.locale, { 70 | useGrouping: this.$props.useGrouping, 71 | minimumFractionDigits: this.$props.precision 72 | }) 73 | }, 74 | computedValue (): string { 75 | if (this.internalValue) { 76 | return ( 77 | (this.$props.prefix ? this.$props.prefix : '') + 78 | this.numberFormatter.format(this.internalValue) 79 | ) 80 | } 81 | return ( 82 | (this.$props.prefix ? this.$props.prefix : '') + 83 | this.numberFormatter.format(0) 84 | ) 85 | }, 86 | computedColor (): string | undefined { 87 | if (this.internalValue < 0 && this.$props.negativeTextColor) { 88 | return this.$props.negativeTextColor 89 | } else return this.$props.color 90 | } 91 | }, 92 | watch: { 93 | value: { 94 | immediate: true, 95 | handler (newVal?: string | number) { 96 | if (!newVal) { 97 | this.internalValue = 0 98 | } else if (typeof newVal === 'string') { 99 | this.internalValue = Number.parseFloat(newVal) 100 | } else { 101 | this.internalValue = newVal 102 | } 103 | }, 104 | deep: true 105 | }, 106 | internalValue (val) { 107 | this.$emit('change-value', val) 108 | }, 109 | computedColor (newVal) { 110 | const input = this.genTextInput() 111 | if (input) { 112 | input.style.color = newVal || null 113 | } 114 | } 115 | }, 116 | methods: { 117 | genTextInput () { 118 | const inputs = this.$el.getElementsByTagName('input') 119 | if (inputs && inputs.length > 0) { 120 | return inputs[0] 121 | } 122 | }, 123 | clearValue () { 124 | this.internalValue = 0 125 | this.fractPart = '0' 126 | this.fractDigitsEdited = false 127 | this.$nextTick(() => { 128 | if (this.$data.value) { 129 | this.internalValue = this.$data.value 130 | } else { 131 | this.internalValue = 0 132 | } 133 | this.$emit('change-value', this.internalValue) 134 | }) 135 | }, 136 | activateCalculator () { 137 | if (!this.$props.readonly) { 138 | this.$emit('activate-calculator', this.internalValue) 139 | } 140 | }, 141 | keyProcess (keyEvent: KeyboardEvent) { 142 | if (!this.isFocused) return 143 | if (keyEvent.key === 'Tab') return 144 | if (this.$props.readonly) { 145 | keyEvent.preventDefault() 146 | keyEvent.stopPropagation() 147 | return 148 | } 149 | if (keyEvent.key !== 'ArrowLeft' && keyEvent.key !== 'ArrowRight') { 150 | keyEvent.preventDefault() 151 | } 152 | keyEvent.stopPropagation() 153 | if (keyEvent.key === this.$props.openKey) { 154 | this.updateDimensions() 155 | this.activateCalculator() 156 | return 157 | } else if (keyEvent.key === 'Delete') { 158 | this.clearValue() 159 | return 160 | } 161 | const numericButtons = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 162 | let strVal = Math.trunc(this.internalValue).toString() 163 | if (numericButtons.includes(keyEvent.key)) { 164 | if (this.fractDigitsEdited) { 165 | this.fractPart += keyEvent.key.toString() 166 | this.fractPart = this.fractPart.substr( 167 | Math.max(0, this.fractPart.length - this.$props.precision), 168 | this.$props.precision 169 | ) 170 | } else { 171 | if (this.clrValue) { 172 | this.fractPart = '00' 173 | strVal = '0' 174 | this.clrValue = false 175 | } 176 | if (strVal === '0' && keyEvent.key !== '0') { 177 | strVal = keyEvent.key 178 | } else if (strVal !== '0') { 179 | strVal += keyEvent.key 180 | } 181 | } 182 | } else if (keyEvent.key === '-') { 183 | if (strVal.startsWith('-')) strVal = strVal.replace('-', '') 184 | else strVal = '-' + strVal 185 | } else if (keyEvent.key === 'Backspace') { 186 | if (this.fractDigitsEdited) { 187 | this.fractPart = 188 | this.fractPart.length <= 1 189 | ? '0' 190 | : this.fractPart.substring(0, this.fractPart.length - 1) 191 | } else { 192 | if (strVal.length === 2 && strVal.startsWith('-')) { 193 | strVal = '0' 194 | } else { 195 | strVal = 196 | strVal.length <= 1 ? '0' : strVal.substring(0, strVal.length - 1) 197 | } 198 | } 199 | } else if ([',', '.'].includes(keyEvent.key)) { 200 | if (this.$props.precision > 0) { 201 | this.fractDigitsEdited = !this.fractDigitsEdited 202 | } 203 | } 204 | if (this.$props.precision > 0) { 205 | strVal = strVal + '.' + this.fractPart 206 | } 207 | let result = Number(strVal) 208 | if (this.$props.precision > 0) { 209 | const p = Math.pow(10, this.$props.precision) 210 | result = Math.round(Number(result) * p) / p 211 | } 212 | result = result = Math.max( 213 | Math.min(this.$props.max, result), 214 | this.$props.min 215 | ) 216 | this.internalValue = result 217 | }, 218 | updateDimensions () { 219 | const rect = this.$el.getBoundingClientRect() 220 | this.$emit('resize-numeric-input', { 221 | bottom: rect.bottom, 222 | right: rect.right 223 | }) 224 | }, 225 | setFocus (val: boolean) { 226 | this.isFocused = val 227 | } 228 | }, 229 | mounted () { 230 | const input = this.genTextInput() 231 | if (input) { 232 | input.setAttribute('type', 'text') 233 | input.style.textAlign = 'right' 234 | } 235 | window.addEventListener('resize', this.updateDimensions) 236 | window.addEventListener('load', this.updateDimensions) 237 | }, 238 | beforeDestroy () { 239 | window.removeEventListener('resize', this.updateDimensions) 240 | window.removeEventListener('load', this.updateDimensions) 241 | }, 242 | render (createElement): VNode { 243 | const currentProps = Object.assign({}, this.$props) 244 | currentProps.value = this.computedValue 245 | if (currentProps.prefix) { 246 | currentProps.prefix = undefined 247 | } 248 | return createElement(VTextFieldA, { 249 | domProps: { 250 | value: this.internalValue 251 | }, 252 | props: currentProps, 253 | on: { 254 | keydown: this.keyProcess, 255 | focus: () => { 256 | this.setFocus(true) 257 | this.fractDigitsEdited = false 258 | this.clrValue = true 259 | }, 260 | blur: () => this.setFocus(false), 261 | 'click:clear': this.clearValue, 262 | input: (val: string) => { 263 | this.internalValue = Number(val) 264 | }, 265 | change: (val: string) => this.$emit('change', val) 266 | } 267 | }, [...(this.$props.useCalculator 268 | ? [ 269 | createElement(VIconA, { 270 | slot: 'append', 271 | attrs: { 272 | tabindex: this.$props.calcNoTabindex ? -1 : 0 273 | }, 274 | on: { 275 | click: () => { 276 | this.updateDimensions() 277 | this.activateCalculator() 278 | } 279 | } 280 | }, this.$props.calcIcon) 281 | ] 282 | : [] 283 | )]) 284 | } 285 | }) 286 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor, Component } from 'vue' 2 | import VNumeric from './VNumeric/VNumeric' 3 | import VNumericInput from './VNumeric/VNumericInput' 4 | import VCalculator from './VNumeric/VCalculator' 5 | 6 | export interface VuetifyNumericUseOptions { 7 | components?: Record; 8 | } 9 | 10 | const defaultComponents = { 11 | 'v-numeric': VNumeric, 12 | 'v-numeric-input': VNumericInput, 13 | 'v-calculator': VCalculator 14 | } 15 | 16 | function install (v: VueConstructor, args?: VuetifyNumericUseOptions): VueConstructor { 17 | const components = args ? args.components : defaultComponents 18 | for (const key in components) { 19 | const component = components[key] 20 | if (component) { 21 | v.component(key, component as typeof v) 22 | } 23 | } 24 | return v 25 | } 26 | 27 | export default install 28 | 29 | export { 30 | VNumeric, 31 | VNumericInput, 32 | VCalculator 33 | } 34 | 35 | if (typeof window !== 'undefined' && window.Vue) { 36 | window.Vue.use(install) 37 | } 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import vuetify from './plugins/vuetify' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | vuetify, 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | 4 | Vue.use(Vuetify) 5 | 6 | export default new Vuetify({ 7 | }) 8 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/shims-vuetify.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import Vue, { VueConstructor } from 'vue' 3 | 4 | import { 5 | VTextField, VBtn, VRow, VSheet, VMenu, VIcon 6 | } from 'vuetify/lib' 7 | 8 | function VueComponent (component: any|undefined, name: string): VueConstructor { 9 | if (component) return component as VueConstructor 10 | return (Vue as any).options.components[name] as VueConstructor 11 | } 12 | 13 | let VTextFieldC 14 | let VBtnC 15 | let VRowC 16 | let VSheetC 17 | let VMenuC 18 | let VIconC 19 | 20 | try { 21 | VBtnC = VBtn 22 | VMenuC = VMenu 23 | VTextFieldC = VTextField 24 | VRowC = VRow 25 | VSheetC = VSheet 26 | VIconC = VIcon 27 | } catch (error) { 28 | VBtnC = undefined 29 | VMenuC = undefined 30 | VTextFieldC = undefined 31 | VRowC = undefined 32 | VSheetC = undefined 33 | VIconC = undefined 34 | } 35 | 36 | export const VBtnA = VueComponent(VBtnC, 'VBtn') 37 | export const VMenuA = VueComponent(VMenuC, 'VMenu') 38 | export const VTextFieldA = VueComponent(VTextFieldC, 'VTextField') 39 | export const VRowA = VueComponent(VRowC, 'VRow') 40 | export const VSheetA = VueComponent(VSheetC, 'VSheet') 41 | export const VIconA = VueComponent(VIconC, 'VIcon') 42 | -------------------------------------------------------------------------------- /testcdn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/unit/VNumeric.spec.ts: -------------------------------------------------------------------------------- 1 | // Libraries 2 | import Vue from 'vue' 3 | import Vuetify from 'vuetify/lib' 4 | // Utilities 5 | import { 6 | mount 7 | } from '@vue/test-utils' 8 | // component to be tested 9 | import VNumeric from '@/components/VNumeric/VNumeric' 10 | 11 | Vue.use(Vuetify) 12 | 13 | const vuetify = new Vuetify({}) 14 | 15 | const value = 1258 16 | const precision = 2 17 | const prefix = '$' 18 | 19 | describe('VNumeric.js', () => { 20 | it('renders', () => { 21 | const wrapper = mount(VNumeric, { 22 | vuetify, 23 | propsData: { 24 | value, 25 | useGrouping: true, 26 | precision, 27 | prefix 28 | } 29 | }) 30 | // wrapper.nextTick() 31 | // const input = wrapper.element.getElementsByTagName('input')[0] 32 | // console.log(input) 33 | // // expect(input.innerText).toMatch('$1,258.00') 34 | expect(wrapper.html()).toMatchSnapshot() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/VNumeric.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`VNumeric.js renders 1`] = ` 4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 | `; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "types": [ 14 | "webpack-env", 15 | "vuetify", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ] 5 | } 6 | 7 | module.exports = { 8 | transpileDependencies: [ 9 | 'vuetify' 10 | ], 11 | publicPath: process.env.NODE_ENV === 'production' 12 | ? '/vuetify-numeric/' 13 | : '/', 14 | configureWebpack: { 15 | ...(process.env.NODE_ENV === 'production' 16 | ? { 17 | externals: { 18 | 'vuetify/lib': 'vuetify/lib' 19 | } 20 | } 21 | : {}) 22 | } 23 | } 24 | --------------------------------------------------------------------------------