Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
neves committed Jun 12, 2017
0 parents commit 87e4b51
Show file tree
Hide file tree
Showing 16 changed files with 2,833 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .babelrc
@@ -0,0 +1,3 @@
{
"presets": ["vue-app"]
}
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules/
3 changes: 3 additions & 0 deletions README.md
@@ -0,0 +1,3 @@
# v-money

Tiny (<2k gzipped) input/directive mask for currency formatting.
41 changes: 41 additions & 0 deletions build.config.js
@@ -0,0 +1,41 @@
const webpack = require.main.require('webpack')
const {name, version} = require('./package.json')

const demo = process.env.NODE_ENV === 'development' || process.env.npm_lifecycle_event === 'docs:build'

module.exports = {
// generate html only for dev and dist:demo
html: demo,
babel: {
babelrc: false,
},
webpack: {
devtool: false, // disable source-map
output: {
publicPath: '.', // generate client.*.js relative to ./demo/index.html
filename: demo ? undefined : kebabCase(name) + '.js', // my-component.js
library: demo ? undefined : camelCase(name) // MyComponent
},
plugins: [
new webpack.DefinePlugin({
'proccess.env.VERSION': JSON.stringify(version) // adds MyComponent.version
})
]
}
}

// utils

// converts MyComponent to my-component
function kebabCase (s) {
return s.replace(/([A-Z])([^A-Z\-])/g, (_, a, b) => `-${a}${b}`)
.toLowerCase()
.replace(/[\s_-]+/g, '-')
.replace(/(^\W)|(\W$)/g, '')
}

// converts my-component to MyComponent
function camelCase (s) {
return s.replace(/([\-_\s]+[a-z])|(^[a-z])/g, $1 => $1.toUpperCase())
.replace(/[\-_\s]+/g, '')
}
1 change: 1 addition & 0 deletions dist/v-money.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions package.json
@@ -0,0 +1,23 @@
{
"name": "v-money",
"version": "0.1.0",
"description": "Vue currency input/directive mask",
"main": "dist/v-money.js",
"scripts": {
"dev": "vue build ./src/docs/docs.vue --config ./src/docs/build.config.js --host 0.0.0.0",
"build": "vue build ./src/index.js --config ./build.config.js --dist ./dist/ --prod --lib",
"docs:build": "vue build ./src/docs/docs.vue --config ./src/docs/build.config.js --dist ./docs/ --prod",
"docs:serve": "open http://localhost:5000/docs/ && serve ./",
"compile": "npm run build -- --disable-compress",
"test": "jest"
},
"keywords": ["vue", "input", "mask", "directive", "currency", "money"],
"files": ["dist/"],
"author": "Marcos Neves <marcos.neves@gmail.com> (https://vuejs-tips.github.io/)",
"license": "MIT",
"devDependencies": {
"babel-jest": "^20.0.3",
"babel-preset-vue-app": "^1.2.0",
"jest": "^20.0.4"
}
}
6 changes: 6 additions & 0 deletions src/assign.js
@@ -0,0 +1,6 @@
export default function (defaults, extras) {
return Object.keys(defaults).concat(Object.keys(extras)).reduce( function (acc, val) {
acc[val] = extras[val] === undefined ? defaults[val] : extras[val]
return acc
}, {})
}
57 changes: 57 additions & 0 deletions src/component.vue
@@ -0,0 +1,57 @@
<template lang="html">
<input type="tel"
v-model="editableValue"
v-money="{precision, decimal, thousands, prefix, suffix}"
style="text-align: right" />
</template>

<script>
import money from './directive'
import defaults from './options'
import {format, unformat} from './utils'
export default {
props: {
value: {
required: true,
type: [Number, String],
default: 0
},
masked: {
type: Boolean,
default: false
},
precision: {
type: Number,
default: () => defaults.precision
},
decimal: {
type: String,
default: () => defaults.decimal
},
thousands: {
type: String,
default: () => defaults.thousands
},
prefix: {
type: String,
default: () => defaults.prefix
},
suffix: {
type: String,
default: () => defaults.suffix
}
},
directives: {money},
computed: {
editableValue: {
get () { return format(this.value, this.$props) },
set (newValue) {
this.$emit('input', this.masked ? newValue : unformat(newValue, this.precision))
}
}
}
}
</script>
41 changes: 41 additions & 0 deletions src/directive.js
@@ -0,0 +1,41 @@
import {format, backspace} from './utils'
import assign from './assign'
import defaults from './options'

function dispatchDelete (e) {
var key = e.keyCode || e.which
if (key === 8 || key === 46) { // Backspace / Delete
e.preventDefault()
e.target.dispatchEvent(new KeyboardEvent('keypress'))
}
}

export default function (el, binding) {
var opt = assign(defaults, binding.value)

el.value = format(el.value, opt)
keepCursorBeforeSuffix()

function keepCursorBeforeSuffix () {
if (el === document.activeElement) {
var index = el.value.length - opt.suffix.length
el.setSelectionRange(index, index)
}
}

function keypress (e) {
e.preventDefault()
var key = e.keyCode || e.which
if (key === 0) { // Backspace / Delete
backspace(el)
}
el.value += String.fromCharCode(key)
el.value = format(el.value, opt)
el.dispatchEvent(new Event('input'))
}

el.onkeydown = dispatchDelete
el.onkeypress = keypress
el.oninput = keepCursorBeforeSuffix
el.dispatchEvent(new Event('input'))
}
16 changes: 16 additions & 0 deletions src/docs/build.config.js
@@ -0,0 +1,16 @@
const path = require('path')

module.exports = {
babel: {
babelrc: false,
},
// html: {
// template: path.resolve(__dirname, './layout.html')
// },
webpack: {
devtool: false, // disable source-map
output: {
publicPath: '', // generate client.*.js relative to ./demo/index.html
}
}
}
92 changes: 92 additions & 0 deletions src/docs/docs.vue
@@ -0,0 +1,92 @@
<template lang="html">
<div class="container col-4 col-xl-6 col-md-8 col-sm-10 col-xs-12">
<label>Component</label>
<div class="columns">
<div class="column col-6 col-sm-12">
<money v-model="price" class="form-input input-lg" v-bind="config"></money>
</div>
<div class="column col-6 col-sm-12">
<h3>{{price}}</h3>
</div>
</div>

<label>Directive</label>
<div class="columns">
<div class="column col-6 col-sm-12">
<input type="tel" v-money="config" v-model="priceDirective" class="form-input input-lg" style="text-align: right" />
</div>
<div class="column col-6 col-sm-12">
<h3>{{priceDirective}}</h3>
</div>
</div>

<div class="columns">
<div class="column col-2 col-sm-3">
<label class="form-label" for="prefix">Prefix</label>
</div>
<div class="column col-2 col-sm-3">
<input class="form-input" type="text" id="prefix" v-model="config.prefix" />
</div>

<div class="column col-2 col-sm-3">
<label class="form-label" for="suffix">Suffix</label>
</div>
<div class="column col-2 col-sm-3">
<input class="form-input" type="text" id="suffix" v-model="config.suffix" />
</div>

<div class="column col-2 col-sm-3">
<label class="form-label" for="precision">Precision</label>
</div>
<div class="column col-2 col-sm-3">
<input class="form-input" type="number" id="precision" v-model.number="config.precision" min="0" max="4" />
</div>

<div class="column col-2 col-sm-3">
<label class="form-label" for="decimal">Decimal</label>
</div>
<div class="column col-2 col-sm-3">
<input class="form-input" type="text" id="decimal" v-model="config.decimal" />
</div>
<div class="column col-2 col-sm-3">
<label class="form-label" for="thousands">Thousands</label>
</div>
<div class="column col-2 col-sm-3">
<input class="form-input" type="text" id="thousands" v-model="config.thousands" />
</div>
<div class="column col-4 col-sm-5">
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="masked" v-model="config.masked" />
<i class="form-icon"></i> Masked Output
</label>
</div>
</div>
</div>
</div>
</template>

<script>
import Vue from 'vue'
import money from '../index'
Vue.use(money)
export default {
data () {
return {
price: 1234.5,
priceDirective: 5432.1,
config: {decimal: ',', thousands: '.', prefix: 'R$ ', suffix: ' #', precision: 2, masked: false}
}
}
}
</script>

<style lang="css">
@import url('https://cdnjs.cloudflare.com/ajax/libs/spectre.css/0.2.14/spectre.min.css');
input {
font-family: monospace;
font-size: 1.8rem !important;
}
</style>
23 changes: 23 additions & 0 deletions src/index.js
@@ -0,0 +1,23 @@
import component from './component'
import directive from './directive'
import options from './options'
const VERSION = proccess.env.VERSION

export {
component,
directive,
options,
VERSION
}

function install (Vue) {
Vue.directive('money', directive)
Vue.component('money', component)
}

export default install

// Install by default if included from script tag
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(install)
}
7 changes: 7 additions & 0 deletions src/options.js
@@ -0,0 +1,7 @@
export default {
prefix: '',
suffix: '',
thousands: ',',
decimal: '.',
precision: 2
}
66 changes: 66 additions & 0 deletions src/utils.js
@@ -0,0 +1,66 @@
import defaults from './options'

function format (input, opt = defaults) {
if (typeof input === 'number') {
input = input.toFixed(fixed(opt.precision))
}
var numbers = onlyNumbers(input)
var currency = numbersToCurrency(numbers, opt.precision)
var parts = toStr(currency).split('.')
var integer = parts[0]
var decimal = parts[1]
integer = addThousandSeparator(integer, opt.thousands)
return opt.prefix + joinIntegerAndDecimal(integer, decimal, opt.decimal) + opt.suffix
}

function unformat (input, precision) {
var numbers = onlyNumbers(input)
var currency = numbersToCurrency(numbers, precision)
return parseFloat(currency)
}

function onlyNumbers (input) {
return toStr(input).replace(/\D+/g, '') || '0'
}

// Uncaught RangeError: toFixed() digits argument must be between 0 and 20 at Number.toFixed
function fixed (precision) {
return between(0, precision, 20)
}

function between (min, n, max) {
return Math.max(min, Math.min(n, max))
}

function numbersToCurrency (numbers, precision) {
var exp = Math.pow(10, precision)
var float = parseFloat(numbers) / exp
return float.toFixed(fixed(precision))
}

function addThousandSeparator (integer, separator) {
return integer.replace(/(\d)(?=(?:\d{3})+\b)/gm, `$1${separator}`)
}

function currencyToIntegerAndDecimal (float) {
return toStr(float).split('.')
}

function joinIntegerAndDecimal (integer, decimal, separator) {
return decimal ? integer + separator + decimal : integer
}

function toStr (value) {
return value ? value.toString() : ''
}

function backspace (el) {
el.value = onlyNumbers(el.value).slice(0, -1)
}

export {
format,
unformat,
backspace,
onlyNumbers
}

0 comments on commit 87e4b51

Please sign in to comment.