Skip to content
Merged
37 changes: 28 additions & 9 deletions docs/.vuepress/components/Example.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@
</q-field>
<div>Model value: <span class="text-bold">{{price}}</span></div>
</div>
<div class="column">
<div class="text-h6">Reverse Fill</div>
<q-field
:dark="false"
dense
outlined
>
<template v-slot:control>
<number
class="q-field__input"
v-model="reverseFill"
v-bind="configReverseFill"
/>
</template>
</q-field>
<div>Model value: <span class="text-bold">{{reverseFill}}</span></div>
</div>
<div class="column">
<div class="text-h6">Directive</div>
<q-field
Expand All @@ -45,7 +62,7 @@
dense
outlined
v-model="priceUnmasked"
v-number="config"
v-number.lazy="config"
/>
<div>Model value: <span class="text-bold">{{priceUnmasked}}</span></div>
</div>
Expand All @@ -69,22 +86,24 @@ export default {
data () {
return {
price: 154.52,
priceDirective: 5432.1,
priceDirective: null,
priceUnmasked: 6789.10,
config: {
decimal: ',',
separator: '.',
prefix: '$',
suffix: '',
suffix: ' %',
precision: 2,
masked: false
null_value: '',
masked: false,
reverseFill: false
},
reverseFill: 6789.10,
configReverseFill: {
reverseFill: true,
suffix: '',
}
}
},
methods: {
change (evt) {
console.log('change', evt.target.value);
}
}
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion docs/.vuepress/enhanceApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
*/

import number from '../../'
import number from '../../src'
import Quasar from 'quasar'
import 'quasar/dist/quasar.min.css'

Expand Down
48 changes: 25 additions & 23 deletions src/component.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
<input
type="text"
autocomplete="off"
:value="formattedValue"
:value="maskedValue"
@change="change"
v-number="{precision, decimal, separator, prefix, suffix}"
@input="input"
v-number="config"
class="v-number"
/>
</template>

<script>
import directive from './directive'
import options from './options'
import { NumberFormat } from './utils'

export default {
props: {
Expand All @@ -28,6 +28,10 @@ export default {
type: Boolean,
default: false
},
reverseFill: {
type: Boolean,
default: options.reverseFill
},
precision: {
type: Number,
default: () => options.precision
Expand All @@ -49,39 +53,37 @@ export default {
default: () => options.suffix
}
},

directives: {
number: directive
},

data() {
return {
formattedValue: ''
maskedValue: this.value,
unmaskedValue: null
}
},

watch: {
masked: {
immediate: true,
deep: true,
handler() {
// console.log('src/component.vue:watch()', val)
const number = new NumberFormat(this.$props).clean()
this.$emit('input', this.masked ? this.formattedValue : number.unformat(this.value))
}
masked() {
this.$emit('input', this.emittedValue)
}
},

methods: {
change(evt) {
// console.log('src/component.vue:change()', evt.target.value)
const number = new NumberFormat(this.$props).clean()
this.$emit('input', this.masked ? number.format(evt.target.value) : number.unformat(evt.target.value))
input({ target }) {
this.maskedValue = target.value
this.unmaskedValue = target.unmaskedValue
this.$emit('input', this.emittedValue)
},
change() {
this.$emit('change', this.emittedValue)
}
},
mounted() {
// console.log('src/component.vue:created()', this.value)
this.formattedValue = new NumberFormat(this.$props).format(this.value)
computed: {
emittedValue() {
return this.masked ? this.maskedValue : this.unmaskedValue
},
config() {
return this.$props
}
}
}
</script>
166 changes: 166 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import NumberFormat from './number-format'
// import options from './options'

export const CONFIG_KEY = '__input-facade__'

/**
* Creates a CustomEvent('input') with detail = { facade: true }
* used as a way to identify our own input event
*/
export function FacadeInputEvent() {
return new CustomEvent('input', {
bubbles: true,
cancelable: true,
detail: { facade: true }
})
}
/**
* Transform an array or string config into an object
*
* @param {object} config The format config object
* @param {object} modifiers An object of modifier flags that can influence the formating process
*/
export function normalizeConfig(defaults, extras) {
defaults = defaults || {}
extras = extras || {}
return Object.keys(defaults).concat(Object.keys(extras)).reduce((acc, val) => {
acc[val] = extras[val] === undefined ? defaults[val] : extras[val]
return acc
}, {})
}

/**
* ensure that the element we're attaching to is an input element
* if not try to find an input element in this elements childrens
*
* @param {HTMLInputElement} el
*/
export function getInputElement(el) {
const inputElement = el instanceof HTMLInputElement ? el : el.querySelector('input')

/* istanbul ignore next */
if (!inputElement) {
throw new Error('facade directive requires an input element')
}

return inputElement
}

/**
* Updates the cursor position to the right place after the masking rule was applied
* @param {HTMLElement} el
* @param {Number} position
*/
export function updateCursor(el, position) {
const setSelectionRange = () => { el.setSelectionRange(position, position) }
if (el === document.activeElement) {
setSelectionRange()
// Android Fix
setTimeout(setSelectionRange, 1)
}
}

/**
* Updates the element's value and unmasked value based on the masking config rules
*
* @param {HTMLInputElement} el The input element to update
* @param {object} [options]
* @param {Boolean} options.emit Wether to dispatch a new InputEvent or not
* @param {Boolean} options.force Forces the update even if the old value and the new value are the same
*/
export function updateValue(el, vnode, { emit = true, force = false, clean = false } = {}) {
const { config } = el[CONFIG_KEY]
let { oldValue } = el[CONFIG_KEY]

let currentValue = vnode && vnode.data.model ? vnode.data.model.value : el.value

oldValue = oldValue || ''
currentValue = currentValue || ''

const number = new NumberFormat(config).clean(clean)
let masked = number.format(currentValue)
let unmasked = number.unformat(currentValue)

// check value with in range max and min value
if (clean) {
if (config.max && unmasked > config.max) {
masked = number.format(config.max)
unmasked = number.unformat(config.max)
} else if (config.min && unmasked < config.min) {
masked = number.format(config.min)
unmasked = number.unformat(config.min)
}
}

if (force || oldValue !== currentValue) {
el[CONFIG_KEY].oldValue = masked
el.unmaskedValue = unmasked
// safari makes the cursor jump to the end if el.value gets assign even if to the same value
if (el.value !== masked) {
el.value = masked
}

// this part needs to be outside the above IF statement for vuetify in firefox
// drawback is that we endup with two's input events in firefox
return emit && el.dispatchEvent(FacadeInputEvent())
}
}

/**
* Input event handler
*
* @param {Event} event The event object
*/
export function inputHandler(event) {
const { target, detail } = event
// We dont need to run this method on the event we emit (prevent event loop)
if (detail && detail.facade) {
return false
}

// since we will be emitting our own custom input event
// we can stop propagation of this native event
event.stopPropagation()

let positionFromEnd = target.value.length - target.selectionEnd
const { oldValue, config } = target[CONFIG_KEY]

updateValue(target, null, { emit: false }, event)
// updated cursor position
positionFromEnd = Math.max(positionFromEnd, config.suffix.length)
positionFromEnd = target.value.length - positionFromEnd
positionFromEnd = Math.max(positionFromEnd, config.prefix.length + 1)
const decimalPosition = target.value.indexOf(config.decimal)
const diff = positionFromEnd - decimalPosition
const maxLength = target.value.length - config.suffix.length
const positionAfterDecimal = positionFromEnd + 1
if (decimalPosition > 0 && diff > 0 && positionAfterDecimal <= maxLength) {
positionFromEnd = positionAfterDecimal
}
updateCursor(target, positionFromEnd)

if (oldValue !== target.value) {
target.dispatchEvent(FacadeInputEvent())
}
}

/**
* Blur event handler
*
* @param {Event} event The event object
*/
export function blurHandler(event) {
const { target, detail } = event
// We dont need to run this method on the event we emit (prevent event loop)
if (detail && detail.facade) {
return false
}

const { oldValue } = target[CONFIG_KEY]

updateValue(target, null, { force: true, clean: true }, event)

if (oldValue !== target.value) {
target.dispatchEvent(FacadeInputEvent())
}
}
Loading