@canutin/svelte-currency-input 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CurrencyInput.svelte +60 -26
- package/CurrencyInput.svelte.d.ts +8 -0
- package/README.md +51 -3
- package/package.json +1 -1
package/CurrencyInput.svelte
CHANGED
|
@@ -3,6 +3,12 @@ const DEFAULT_CURRENCY = 'USD';
|
|
|
3
3
|
const DEFAULT_NAME = 'total';
|
|
4
4
|
const DEFAULT_VALUE = 0;
|
|
5
5
|
const DEFAULT_FRACTION_DIGITS = 2;
|
|
6
|
+
const DEFAULT_CLASS_WRAPPER = 'currencyInput';
|
|
7
|
+
const DEFAULT_CLASS_UNFORMATTED = 'currencyInput__unformatted';
|
|
8
|
+
const DEFAULT_CLASS_FORMATTED = 'currencyInput__formatted';
|
|
9
|
+
const DEFAULT_CLASS_FORMATTED_POSITIVE = 'currencyInput__formatted--positive';
|
|
10
|
+
const DEFAULT_CLASS_FORMATTED_NEGATIVE = 'currencyInput__formatted--negative';
|
|
11
|
+
const DEFAULT_CLASS_FORMATTED_ZERO = 'currencyInput__formatted--zero';
|
|
6
12
|
export let value = DEFAULT_VALUE;
|
|
7
13
|
export let locale = DEFAULT_LOCALE;
|
|
8
14
|
export let currency = DEFAULT_CURRENCY;
|
|
@@ -12,6 +18,7 @@ export let disabled = false;
|
|
|
12
18
|
export let placeholder = DEFAULT_VALUE;
|
|
13
19
|
export let isNegativeAllowed = true;
|
|
14
20
|
export let fractionDigits = DEFAULT_FRACTION_DIGITS;
|
|
21
|
+
export let inputClasses = null;
|
|
15
22
|
// Formats value as: e.g. $1,523.00 | -$1,523.00
|
|
16
23
|
const formatCurrency = (value, maximumFractionDigits, minimumFractionDigits) => {
|
|
17
24
|
return new Intl.NumberFormat(locale, {
|
|
@@ -26,8 +33,9 @@ const handleKeyDown = (event) => {
|
|
|
26
33
|
const isDeletion = event.key === 'Backspace' || event.key === 'Delete';
|
|
27
34
|
const isModifier = event.metaKey || event.altKey || event.ctrlKey;
|
|
28
35
|
const isArrowKey = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
36
|
+
const isTab = event.key === 'Tab';
|
|
29
37
|
const isInvalidCharacter = !/^\d|,|\.|-$/g.test(event.key); // Keys that are not a digit, comma, period or minus sign
|
|
30
|
-
if (!isDeletion && !isModifier && !isArrowKey && isInvalidCharacter)
|
|
38
|
+
if (!isDeletion && !isModifier && !isArrowKey && isInvalidCharacter && !isTab)
|
|
31
39
|
event.preventDefault();
|
|
32
40
|
};
|
|
33
41
|
let inputTarget;
|
|
@@ -38,29 +46,31 @@ const currencySymbol = formatCurrency(0, 0)
|
|
|
38
46
|
.replace(/\u00A0/, ''); // e.g '0 €' > '€'
|
|
39
47
|
// Updates `value` by stripping away the currency formatting
|
|
40
48
|
const setUnformattedValue = (event) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
if (event) {
|
|
50
|
+
// Don't format if the user is typing a `currencyDecimal` point
|
|
51
|
+
if (event.key === currencyDecimal)
|
|
52
|
+
return;
|
|
53
|
+
// Pressing `.` when the decimal point is `,` gets replaced with `,`
|
|
54
|
+
if (isDecimalComma && event.key === '.')
|
|
55
|
+
formattedValue = formattedValue.replace(/\.([^.]*)$/, currencyDecimal + '$1'); // Only replace the last occurence
|
|
56
|
+
// Pressing `,` when the decimal point is `.` gets replaced with `.`
|
|
57
|
+
if (!isDecimalComma && event.key === ',')
|
|
58
|
+
formattedValue = formattedValue.replace(/\,([^,]*)$/, currencyDecimal + '$1'); // Only replace the last occurence
|
|
59
|
+
// Don't format if `formattedValue` is ['$', '-$', "-"]
|
|
60
|
+
const ignoreSymbols = [currencySymbol, `-${currencySymbol}`, '-'];
|
|
61
|
+
const strippedUnformattedValue = formattedValue.replace(' ', '');
|
|
62
|
+
if (ignoreSymbols.includes(strippedUnformattedValue))
|
|
63
|
+
return;
|
|
64
|
+
// Set the starting caret positions
|
|
65
|
+
inputTarget = event.target;
|
|
66
|
+
// Reverse the value when minus is pressed
|
|
67
|
+
if (isNegativeAllowed && event.key === '-')
|
|
68
|
+
value = value * -1;
|
|
69
|
+
}
|
|
57
70
|
// Remove all characters that arent: numbers, commas, periods (or minus signs if `isNegativeAllowed`)
|
|
58
71
|
let unformattedValue = isNegativeAllowed
|
|
59
72
|
? formattedValue.replace(/[^0-9,.-]/g, '')
|
|
60
73
|
: formattedValue.replace(/[^0-9,.]/g, '');
|
|
61
|
-
// Reverse the value when minus is pressed
|
|
62
|
-
if (isNegativeAllowed && event.key === '-')
|
|
63
|
-
value = value * -1;
|
|
64
74
|
// Finally set the value
|
|
65
75
|
if (Number.isNaN(parseFloat(unformattedValue))) {
|
|
66
76
|
value = 0;
|
|
@@ -70,7 +80,18 @@ const setUnformattedValue = (event) => {
|
|
|
70
80
|
unformattedValue = unformattedValue.replace(isDecimalComma ? /\./g : /\,/g, ''); // Remove all group symbols
|
|
71
81
|
if (isDecimalComma)
|
|
72
82
|
unformattedValue = unformattedValue.replace(',', '.'); // If the decimal point is a comma, replace it with a period
|
|
83
|
+
// If the zero-key has been pressed
|
|
84
|
+
// and if the current `value` is the same as the `value` before the key-press
|
|
85
|
+
// formatting may need to be done (Issue #30)
|
|
86
|
+
const previousValue = value;
|
|
73
87
|
value = parseFloat(unformattedValue);
|
|
88
|
+
if (event && previousValue === value) {
|
|
89
|
+
// Do the formatting if the number of digits after the decimal point exceeds `fractionDigits`
|
|
90
|
+
if (unformattedValue.includes('.') &&
|
|
91
|
+
unformattedValue.split('.')[1].length > fractionDigits) {
|
|
92
|
+
setFormattedValue();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
};
|
|
76
97
|
const setFormattedValue = () => {
|
|
@@ -79,6 +100,8 @@ const setFormattedValue = () => {
|
|
|
79
100
|
const previousFormattedValueLength = formattedValue.length;
|
|
80
101
|
// Apply formatting to input
|
|
81
102
|
formattedValue = isZero ? '' : formatCurrency(value, fractionDigits, 0);
|
|
103
|
+
// Update `value` after formatting
|
|
104
|
+
setUnformattedValue();
|
|
82
105
|
// New caret position
|
|
83
106
|
const endCaretPosition = startCaretPosition + formattedValue.length - previousFormattedValueLength;
|
|
84
107
|
// HACK:
|
|
@@ -94,14 +117,24 @@ $: isNegative = value < 0;
|
|
|
94
117
|
$: value, setFormattedValue();
|
|
95
118
|
</script>
|
|
96
119
|
|
|
97
|
-
<div class=
|
|
98
|
-
<input
|
|
120
|
+
<div class={inputClasses?.wrapper ?? DEFAULT_CLASS_WRAPPER}>
|
|
121
|
+
<input
|
|
122
|
+
class={inputClasses?.unformatted ?? DEFAULT_CLASS_UNFORMATTED}
|
|
123
|
+
type="hidden"
|
|
124
|
+
{name}
|
|
125
|
+
{disabled}
|
|
126
|
+
bind:value
|
|
127
|
+
/>
|
|
99
128
|
<input
|
|
100
129
|
class="
|
|
101
|
-
|
|
102
|
-
{isNegativeAllowed && !isZero && !isNegative
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
{inputClasses?.formatted ?? DEFAULT_CLASS_FORMATTED}
|
|
131
|
+
{isNegativeAllowed && !isZero && !isNegative
|
|
132
|
+
? inputClasses?.formattedPositive ?? DEFAULT_CLASS_FORMATTED_POSITIVE
|
|
133
|
+
: ''}
|
|
134
|
+
{isZero ? inputClasses?.formattedZero ?? DEFAULT_CLASS_FORMATTED_ZERO : ''}
|
|
135
|
+
{isNegativeAllowed && isNegative
|
|
136
|
+
? inputClasses?.formattedNegative ?? DEFAULT_CLASS_FORMATTED_NEGATIVE
|
|
137
|
+
: ''}
|
|
105
138
|
"
|
|
106
139
|
type="text"
|
|
107
140
|
inputmode="numeric"
|
|
@@ -112,6 +145,7 @@ $: value, setFormattedValue();
|
|
|
112
145
|
bind:value={formattedValue}
|
|
113
146
|
on:keydown={handleKeyDown}
|
|
114
147
|
on:keyup={setUnformattedValue}
|
|
148
|
+
on:blur={setFormattedValue}
|
|
115
149
|
/>
|
|
116
150
|
</div>
|
|
117
151
|
|
|
@@ -10,6 +10,14 @@ declare const __propDef: {
|
|
|
10
10
|
placeholder?: number | null | undefined;
|
|
11
11
|
isNegativeAllowed?: boolean | undefined;
|
|
12
12
|
fractionDigits?: number | undefined;
|
|
13
|
+
inputClasses?: {
|
|
14
|
+
wrapper?: string | undefined;
|
|
15
|
+
unformatted?: string | undefined;
|
|
16
|
+
formatted?: string | undefined;
|
|
17
|
+
formattedPositive?: string | undefined;
|
|
18
|
+
formattedNegative?: string | undefined;
|
|
19
|
+
formattedZero?: string | undefined;
|
|
20
|
+
} | null | undefined;
|
|
13
21
|
};
|
|
14
22
|
events: {
|
|
15
23
|
[evt: string]: CustomEvent<any>;
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A form input that converts numbers to localized currency formats as you type
|
|
|
15
15
|
- Formats **positive** and **negative** values
|
|
16
16
|
- Leverages [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for **localizing** currency denominations and masking the input
|
|
17
17
|
- Simple [API](#api)
|
|
18
|
-
- Minimal
|
|
18
|
+
- Minimal default styling, easy to [customize](#styling)
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
@@ -69,12 +69,43 @@ This is more or less what `<CurrencyInput />` looks like under the hood:
|
|
|
69
69
|
| placeholder | `number` `null` | `0` | Overrides the default placeholder. Setting the value to a `number` will display it as formatted. Setting it to `null` will not show a placeholder |
|
|
70
70
|
| isNegativeAllowed | `boolean` | `true` | If `false`, forces formatting only to positive values and ignores `--positive` and `--negative` styling modifiers |
|
|
71
71
|
| fractionDigits | `number` | `2` | Sets `maximumFractionDigits` in [`Intl.NumberFormat()` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits) used for formatting the currency. Supported digits: `0` to `20` |
|
|
72
|
+
| inputClasses | `object` | [See below](#Styling) | Selectively overrides any class names passed |
|
|
72
73
|
|
|
73
74
|
## Styling
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
There are two ways of customizing the styling of the input:
|
|
77
|
+
1. Passing it your own CSS classes
|
|
78
|
+
2. Overriding the styles using the existing class names
|
|
79
|
+
|
|
80
|
+
You can **override all of the class names** by passing an object to `inputClasses` that has **one or more** of these properties:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface InputClasses {
|
|
84
|
+
wrapper?: string; // <div> that contains the two <input> elements
|
|
85
|
+
unformatted?: string; // <input type="hidden"> that contains the unformatted value
|
|
86
|
+
formatted?: string; // <input type="text"> that contains the formatted value
|
|
87
|
+
formattedPositive?: string; // Class added when the formatted input is positive
|
|
88
|
+
formattedNegative?: string; // Class added when the formatted input is negative
|
|
89
|
+
formattedZero?: string; // Class added when the formatted input is zero
|
|
90
|
+
}
|
|
91
|
+
```
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
Usage (with [Tailwind CSS](https://tailwindcss.com/) as an example):
|
|
94
|
+
|
|
95
|
+
```svelte
|
|
96
|
+
<CurrencyInput name="total" value="{420.69}" inputClasses={
|
|
97
|
+
{
|
|
98
|
+
wrapper: "form-control block",
|
|
99
|
+
formatted: 'py-1.5 text-gray-700',
|
|
100
|
+
formattedPositive: 'text-green-700',
|
|
101
|
+
formattedNegative: 'text-red-700'
|
|
102
|
+
}
|
|
103
|
+
} />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Alternatively you can **write your own CSS** by overriding the [default styles](https://github.com/canutin/svelte-currency-input/blob/main/src/lib/CurrencyInput.svelte) which use [BEM naming conventions](https://getbem.com/naming/). To do so apply your styles as shown below:
|
|
107
|
+
|
|
108
|
+
```svelte
|
|
78
109
|
<div class="my-currency-input">
|
|
79
110
|
<CurrencyInput name="total" value="{420.69}" />
|
|
80
111
|
</div>
|
|
@@ -118,3 +149,20 @@ npm run dev
|
|
|
118
149
|
# or start the server and open the app in a new browser tab
|
|
119
150
|
npm run dev -- --open
|
|
120
151
|
```
|
|
152
|
+
|
|
153
|
+
#### Integration tests
|
|
154
|
+
|
|
155
|
+
The component is tested using [Playwright](https://playwright.dev/).
|
|
156
|
+
You can find the tests in [`tests/svelte-currency-input.test.ts`](https://github.com/Canutin/svelte-currency-input/blob/main/tests/svelte-currency-input.test.ts)
|
|
157
|
+
|
|
158
|
+
To run all tests on **Chromium**, **Firefox** and **Webkit**:
|
|
159
|
+
```bash
|
|
160
|
+
npm run test
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
To run all tests on a specific browser (e.g. **Webkit**):
|
|
164
|
+
```bash
|
|
165
|
+
npx playwright test --project=webkit
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Additional debug commands can be found on [Playwright's documentation](https://playwright.dev/docs/test-cli).
|