Skip to content

Commit 9e9840b

Browse files
authored
Merge pull request #1 from coders-tm/dev
Dev
2 parents 7b9659b + e756cbe commit 9e9840b

14 files changed

+609
-421
lines changed

docs/.vuepress/components/Example.vue

+28-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919
</q-field>
2020
<div>Model value: <span class="text-bold">{{price}}</span></div>
2121
</div>
22+
<div class="column">
23+
<div class="text-h6">Reverse Fill</div>
24+
<q-field
25+
:dark="false"
26+
dense
27+
outlined
28+
>
29+
<template v-slot:control>
30+
<number
31+
class="q-field__input"
32+
v-model="reverseFill"
33+
v-bind="configReverseFill"
34+
/>
35+
</template>
36+
</q-field>
37+
<div>Model value: <span class="text-bold">{{reverseFill}}</span></div>
38+
</div>
2239
<div class="column">
2340
<div class="text-h6">Directive</div>
2441
<q-field
@@ -45,7 +62,7 @@
4562
dense
4663
outlined
4764
v-model="priceUnmasked"
48-
v-number="config"
65+
v-number.lazy="config"
4966
/>
5067
<div>Model value: <span class="text-bold">{{priceUnmasked}}</span></div>
5168
</div>
@@ -69,22 +86,24 @@ export default {
6986
data () {
7087
return {
7188
price: 154.52,
72-
priceDirective: 5432.1,
89+
priceDirective: null,
7390
priceUnmasked: 6789.10,
7491
config: {
7592
decimal: ',',
7693
separator: '.',
7794
prefix: '$',
78-
suffix: '',
95+
suffix: ' %',
7996
precision: 2,
80-
masked: false
97+
null_value: '',
98+
masked: false,
99+
reverseFill: false
100+
},
101+
reverseFill: 6789.10,
102+
configReverseFill: {
103+
reverseFill: true,
104+
suffix: '',
81105
}
82106
}
83-
},
84-
methods: {
85-
change (evt) {
86-
console.log('change', evt.target.value);
87-
}
88107
}
89108
}
90109
</script>

docs/.vuepress/enhanceApp.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
55
*/
66

7-
import number from '../../'
7+
import number from '../../src'
88
import Quasar from 'quasar'
99
import 'quasar/dist/quasar.min.css'
1010

src/component.vue

+25-23
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
<input
33
type="text"
44
autocomplete="off"
5-
:value="formattedValue"
5+
:value="maskedValue"
66
@change="change"
7-
v-number="{precision, decimal, separator, prefix, suffix}"
7+
@input="input"
8+
v-number="config"
89
class="v-number"
910
/>
1011
</template>
1112

1213
<script>
1314
import directive from './directive'
1415
import options from './options'
15-
import { NumberFormat } from './utils'
1616
1717
export default {
1818
props: {
@@ -28,6 +28,10 @@ export default {
2828
type: Boolean,
2929
default: false
3030
},
31+
reverseFill: {
32+
type: Boolean,
33+
default: options.reverseFill
34+
},
3135
precision: {
3236
type: Number,
3337
default: () => options.precision
@@ -49,39 +53,37 @@ export default {
4953
default: () => options.suffix
5054
}
5155
},
52-
5356
directives: {
5457
number: directive
5558
},
56-
5759
data() {
5860
return {
59-
formattedValue: ''
61+
maskedValue: this.value,
62+
unmaskedValue: null
6063
}
6164
},
62-
6365
watch: {
64-
masked: {
65-
immediate: true,
66-
deep: true,
67-
handler() {
68-
// console.log('src/component.vue:watch()', val)
69-
const number = new NumberFormat(this.$props).clean()
70-
this.$emit('input', this.masked ? this.formattedValue : number.unformat(this.value))
71-
}
66+
masked() {
67+
this.$emit('input', this.emittedValue)
7268
}
7369
},
74-
7570
methods: {
76-
change(evt) {
77-
// console.log('src/component.vue:change()', evt.target.value)
78-
const number = new NumberFormat(this.$props).clean()
79-
this.$emit('input', this.masked ? number.format(evt.target.value) : number.unformat(evt.target.value))
71+
input({ target }) {
72+
this.maskedValue = target.value
73+
this.unmaskedValue = target.unmaskedValue
74+
this.$emit('input', this.emittedValue)
75+
},
76+
change() {
77+
this.$emit('change', this.emittedValue)
8078
}
8179
},
82-
mounted() {
83-
// console.log('src/component.vue:created()', this.value)
84-
this.formattedValue = new NumberFormat(this.$props).format(this.value)
80+
computed: {
81+
emittedValue() {
82+
return this.masked ? this.maskedValue : this.unmaskedValue
83+
},
84+
config() {
85+
return this.$props
86+
}
8587
}
8688
}
8789
</script>

src/core.js

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import NumberFormat from './number-format'
2+
// import options from './options'
3+
4+
export const CONFIG_KEY = '__input-facade__'
5+
6+
/**
7+
* Creates a CustomEvent('input') with detail = { facade: true }
8+
* used as a way to identify our own input event
9+
*/
10+
export function FacadeInputEvent() {
11+
return new CustomEvent('input', {
12+
bubbles: true,
13+
cancelable: true,
14+
detail: { facade: true }
15+
})
16+
}
17+
/**
18+
* Transform an array or string config into an object
19+
*
20+
* @param {object} config The format config object
21+
* @param {object} modifiers An object of modifier flags that can influence the formating process
22+
*/
23+
export function normalizeConfig(defaults, extras) {
24+
defaults = defaults || {}
25+
extras = extras || {}
26+
return Object.keys(defaults).concat(Object.keys(extras)).reduce((acc, val) => {
27+
acc[val] = extras[val] === undefined ? defaults[val] : extras[val]
28+
return acc
29+
}, {})
30+
}
31+
32+
/**
33+
* ensure that the element we're attaching to is an input element
34+
* if not try to find an input element in this elements childrens
35+
*
36+
* @param {HTMLInputElement} el
37+
*/
38+
export function getInputElement(el) {
39+
const inputElement = el instanceof HTMLInputElement ? el : el.querySelector('input')
40+
41+
/* istanbul ignore next */
42+
if (!inputElement) {
43+
throw new Error('facade directive requires an input element')
44+
}
45+
46+
return inputElement
47+
}
48+
49+
/**
50+
* Updates the cursor position to the right place after the masking rule was applied
51+
* @param {HTMLElement} el
52+
* @param {Number} position
53+
*/
54+
export function updateCursor(el, position) {
55+
const setSelectionRange = () => { el.setSelectionRange(position, position) }
56+
if (el === document.activeElement) {
57+
setSelectionRange()
58+
// Android Fix
59+
setTimeout(setSelectionRange, 1)
60+
}
61+
}
62+
63+
/**
64+
* Updates the element's value and unmasked value based on the masking config rules
65+
*
66+
* @param {HTMLInputElement} el The input element to update
67+
* @param {object} [options]
68+
* @param {Boolean} options.emit Wether to dispatch a new InputEvent or not
69+
* @param {Boolean} options.force Forces the update even if the old value and the new value are the same
70+
*/
71+
export function updateValue(el, vnode, { emit = true, force = false, clean = false } = {}) {
72+
const { config } = el[CONFIG_KEY]
73+
let { oldValue } = el[CONFIG_KEY]
74+
75+
let currentValue = vnode && vnode.data.model ? vnode.data.model.value : el.value
76+
77+
oldValue = oldValue || ''
78+
currentValue = currentValue || ''
79+
80+
const number = new NumberFormat(config).clean(clean)
81+
let masked = number.format(currentValue)
82+
let unmasked = number.unformat(currentValue)
83+
84+
// check value with in range max and min value
85+
if (clean) {
86+
if (config.max && unmasked > config.max) {
87+
masked = number.format(config.max)
88+
unmasked = number.unformat(config.max)
89+
} else if (config.min && unmasked < config.min) {
90+
masked = number.format(config.min)
91+
unmasked = number.unformat(config.min)
92+
}
93+
}
94+
95+
if (force || oldValue !== currentValue) {
96+
el[CONFIG_KEY].oldValue = masked
97+
el.unmaskedValue = unmasked
98+
// safari makes the cursor jump to the end if el.value gets assign even if to the same value
99+
if (el.value !== masked) {
100+
el.value = masked
101+
}
102+
103+
// this part needs to be outside the above IF statement for vuetify in firefox
104+
// drawback is that we endup with two's input events in firefox
105+
return emit && el.dispatchEvent(FacadeInputEvent())
106+
}
107+
}
108+
109+
/**
110+
* Input event handler
111+
*
112+
* @param {Event} event The event object
113+
*/
114+
export function inputHandler(event) {
115+
const { target, detail } = event
116+
// We dont need to run this method on the event we emit (prevent event loop)
117+
if (detail && detail.facade) {
118+
return false
119+
}
120+
121+
// since we will be emitting our own custom input event
122+
// we can stop propagation of this native event
123+
event.stopPropagation()
124+
125+
let positionFromEnd = target.value.length - target.selectionEnd
126+
const { oldValue, config } = target[CONFIG_KEY]
127+
128+
updateValue(target, null, { emit: false }, event)
129+
// updated cursor position
130+
positionFromEnd = Math.max(positionFromEnd, config.suffix.length)
131+
positionFromEnd = target.value.length - positionFromEnd
132+
positionFromEnd = Math.max(positionFromEnd, config.prefix.length + 1)
133+
const decimalPosition = target.value.indexOf(config.decimal)
134+
const diff = positionFromEnd - decimalPosition
135+
const maxLength = target.value.length - config.suffix.length
136+
const positionAfterDecimal = positionFromEnd + 1
137+
if (decimalPosition > 0 && diff > 0 && positionAfterDecimal <= maxLength) {
138+
positionFromEnd = positionAfterDecimal
139+
}
140+
updateCursor(target, positionFromEnd)
141+
142+
if (oldValue !== target.value) {
143+
target.dispatchEvent(FacadeInputEvent())
144+
}
145+
}
146+
147+
/**
148+
* Blur event handler
149+
*
150+
* @param {Event} event The event object
151+
*/
152+
export function blurHandler(event) {
153+
const { target, detail } = event
154+
// We dont need to run this method on the event we emit (prevent event loop)
155+
if (detail && detail.facade) {
156+
return false
157+
}
158+
159+
const { oldValue } = target[CONFIG_KEY]
160+
161+
updateValue(target, null, { force: true, clean: true }, event)
162+
163+
if (oldValue !== target.value) {
164+
target.dispatchEvent(FacadeInputEvent())
165+
}
166+
}

0 commit comments

Comments
 (0)