Skip to content

Commit 5a46ec6

Browse files
author
Dipak Sarkar
committed
updated directive and functions
1 parent 7b9659b commit 5a46ec6

File tree

10 files changed

+185
-111
lines changed

10 files changed

+185
-111
lines changed

docs/.vuepress/components/Example.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export default {
7272
priceDirective: 5432.1,
7373
priceUnmasked: 6789.10,
7474
config: {
75-
decimal: ',',
76-
separator: '.',
75+
decimal: '.',
76+
separator: ',',
7777
prefix: '$',
7878
suffix: '',
7979
precision: 2,

docs/.vuepress/enhanceApp.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
<input
33
type="text"
44
autocomplete="off"
5-
:value="formattedValue"
5+
:value="maskedValue"
66
@change="change"
7+
@input="input"
78
v-number="{precision, decimal, separator, prefix, suffix}"
89
class="v-number"
910
/>
@@ -12,7 +13,6 @@
1213
<script>
1314
import directive from './directive'
1415
import options from './options'
15-
import { NumberFormat } from './utils'
1616
1717
export default {
1818
props: {
@@ -49,39 +49,34 @@ export default {
4949
default: () => options.suffix
5050
}
5151
},
52-
5352
directives: {
5453
number: directive
5554
},
56-
5755
data() {
5856
return {
59-
formattedValue: ''
57+
maskedValue: this.value,
58+
unmaskedValue: null
6059
}
6160
},
62-
6361
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-
}
62+
masked() {
63+
this.$emit('input', this.emittedValue)
7264
}
7365
},
74-
7566
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))
67+
input({ target }) {
68+
this.maskedValue = target.value
69+
this.unmaskedValue = target.unmaskedValue
70+
this.$emit('input', this.emittedValue)
71+
},
72+
change() {
73+
this.$emit('change', this.emittedValue)
8074
}
8175
},
82-
mounted() {
83-
// console.log('src/component.vue:created()', this.value)
84-
this.formattedValue = new NumberFormat(this.$props).format(this.value)
76+
computed: {
77+
emittedValue() {
78+
return this.masked ? this.maskedValue : this.unmaskedValue
79+
}
8580
}
8681
}
8782
</script>

src/core.js

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

src/directive.js

Lines changed: 33 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,43 @@
1-
import options from './options';
2-
import { NumberFormat, setCursor } from './utils'
1+
import * as core from './core'
32

4-
export default function (el, binding) {
5-
const { value } = binding
6-
if (!value) return false
7-
const config = Object.assign(options, value)
8-
// console.log('src/components/directive:init()', config)
3+
const CONFIG_KEY = core.CONFIG_KEY
94

10-
// v-number used on a component that's not a input element
11-
if (el.localName !== 'input') {
12-
const input = el.getElementsByTagName('input')
13-
if (input.length < 1) {
14-
throw new Error(`v-number requires 1 input element, found ${input.length}`)
15-
} else {
16-
el = input[0]
17-
}
18-
}
5+
export default {
6+
bind: (el, { value }, vnode) => {
7+
el = core.getInputElement(el)
8+
const config = core.normalizeConfig(value)
9+
el[CONFIG_KEY] = { config }
10+
// set initial value
11+
core.updateValue(el, vnode, { force: config.prefill })
12+
},
1913

20-
// change the input type to {text} because we can't use it to any other input types
21-
el.setAttribute('type', 'text')
14+
inserted: (el) => {
15+
el = core.getInputElement(el)
16+
const config = el[CONFIG_KEY]
17+
// prefer adding event listener to parent element to avoid Firefox bug which does not
18+
// execute `useCapture: true` event handlers before non-capturing event handlers
19+
const handlerOwner = el.parentElement || el
2220

23-
el.oninput = () => {
24-
// console.log('src/directive.js:oninput()', evt)
25-
var positionFromEnd = el.value.length - el.selectionEnd
26-
el.value = new NumberFormat(config).format(el.value)
27-
positionFromEnd = Math.max(positionFromEnd, config.suffix.length)
28-
positionFromEnd = el.value.length - positionFromEnd
29-
positionFromEnd = Math.max(positionFromEnd, config.prefix.length + 1)
30-
setCursor(el, positionFromEnd)
31-
}
21+
// use anonymous event handler to avoid inadvertently removing masking for all inputs within a container
22+
const handler = (e) => core.inputHandler(e)
3223

33-
el.onblur = () => {
34-
// clean up after end the input
35-
// console.log('src/directive.js:onblur()')
36-
el.value = new NumberFormat(config).clean().format(el.value)
37-
el.dispatchEvent(new Event('change'))
38-
}
24+
handlerOwner.addEventListener('input', handler, true)
3925

40-
el.onfocus = () => {
41-
// console.log('src/directive.js:onfocus()')
42-
setCursor(el, el.value.length - config.suffix.length)
43-
}
26+
config.cleanup = () => handlerOwner.removeEventListener('input', handler, true)
27+
},
4428

45-
el.onkeydown = (evt) => {
46-
// console.log('src/directive.js:onkeydown()')
47-
// Check deciaml
48-
if (evt.key === config.decimal && evt.target.value.includes(config.decimal)) {
49-
evt.preventDefault()
50-
}
51-
// Allow these keys only
52-
if (
53-
// backspace, delete, tab, escape, enter
54-
[46, 8, 9, 27, 13].indexOf(evt.keyCode) >= 0
55-
// Ctrl/cmd+A
56-
|| (evt.keyCode === 65 && (evt.ctrlKey || evt.metaKey))
57-
// Ctrl/cmd+C
58-
|| (evt.keyCode === 67 && (evt.ctrlKey || evt.metaKey))
59-
// Ctrl/cmd+V
60-
|| (evt.keyCode === 86 && (evt.ctrlKey || evt.metaKey))
61-
// Ctrl/cmd+R
62-
|| (evt.keyCode === 82 && (evt.ctrlKey || evt.metaKey))
63-
// Ctrl/cmd+X
64-
|| (evt.keyCode === 88 && (evt.ctrlKey || evt.metaKey))
65-
// home, end, left, right
66-
|| (evt.keyCode >= 35 && evt.keyCode <= 39)
67-
|| (evt.keyCode >= 48 && evt.keyCode <= 57)
68-
|| evt.keyCode === 109
69-
|| evt.key === config.decimal
70-
) {
71-
return true
29+
update: (el, { value, oldValue }, vnode) => {
30+
el = core.getInputElement(el)
31+
32+
if (value !== oldValue) {
33+
el[CONFIG_KEY].config = core.normalizeConfig(value)
34+
core.updateValue(el, vnode, { force: true })
35+
} else {
36+
core.updateValue(el, vnode)
7237
}
73-
evt.preventDefault()
74-
}
38+
},
7539

76-
// force format after initialization
77-
el.oninput()
78-
el.dispatchEvent(new Event('input'))
40+
unbind: (el) => {
41+
core.getInputElement(el)[CONFIG_KEY].cleanup()
42+
}
7943
}

src/utils.js renamed to src/number-format.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import options from './options'
22

3-
function NumberFormat(opt = options) {
3+
/**
4+
* Number format function
5+
* @param {Object} options
6+
*/
7+
export default function NumberFormat(opt = options) {
48
this.options = Object.assign(options, opt)
59
this.input = this.options.null_value
610
this.number = this.options.null_value
@@ -59,16 +63,3 @@ function NumberFormat(opt = options) {
5963
return this.negative() + this.realNumber()
6064
}
6165
}
62-
63-
function setCursor(el, position) {
64-
const setSelectionRange = () => { el.setSelectionRange(position, position) }
65-
if (el === document.activeElement) {
66-
setSelectionRange()
67-
setTimeout(setSelectionRange, 1) // Android Fix
68-
}
69-
}
70-
71-
export {
72-
NumberFormat,
73-
setCursor
74-
}

src/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export default {
44
separator: ',',
55
decimal: '.',
66
precision: 2,
7+
prefill: true,
78
null_value: 0
89
}

tests/unit/directive.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import directive from '../../src/directive'
22

33
test ('should not throw error on empty config', () => {
4-
expect(() => directive({}, {})).not.toThrow()
4+
expect(() => directive({}, {})).toThrow()
55
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NumberFormat } from '../../src/utils'
1+
import NumberFormat from '../../src/number-format'
22

33
describe('should not throw error on empty config', () => {
44
expect(() => new NumberFormat({
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NumberFormat } from '../../src/utils'
1+
import NumberFormat from '../../src/number-format'
22

33
describe('should not throw error on empty config', () => {
44
expect(() => new NumberFormat({})).not.toThrow()

0 commit comments

Comments
 (0)