@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.
@@ -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
- // Don't format if the user is typing a `currencyDecimal` point
42
- if (event.key === currencyDecimal)
43
- return;
44
- // Pressing `.` when the decimal point is `,` gets replaced with `,`
45
- if (isDecimalComma && event.key === '.')
46
- formattedValue = formattedValue.replace(/\.([^.]*)$/, currencyDecimal + '$1'); // Only replace the last occurence
47
- // Pressing `,` when the decimal point is `.` gets replaced with `.`
48
- if (!isDecimalComma && event.key === ',')
49
- formattedValue = formattedValue.replace(/\,([^,]*)$/, currencyDecimal + '$1'); // Only replace the last occurence
50
- // Don't format if `formattedValue` is ['$', '-$', "-"]
51
- const ignoreSymbols = [currencySymbol, `-${currencySymbol}`, '-'];
52
- const strippedUnformattedValue = formattedValue.replace(' ', '');
53
- if (ignoreSymbols.includes(strippedUnformattedValue))
54
- return;
55
- // Set the starting caret positions
56
- inputTarget = event.target;
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="currencyInput">
98
- <input class="currencyInput__unformatted" type="hidden" {name} {disabled} bind:value />
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
- currencyInput__formatted
102
- {isNegativeAllowed && !isZero && !isNegative && 'currencyInput__formatted--positive'}
103
- {isZero && 'currencyInput__formatted--zero'}
104
- {isNegativeAllowed && isNegative && 'currencyInput__formatted--negative'}
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 [default styling](https://github.com/canutin/svelte-currency-input/blob/main/src/lib/CurrencyInput.svelte#L88-L118), easy to [customize](#styling)
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
- The [default styles](https://github.com/canutin/svelte-currency-input/blob/main/src/lib/CurrencyInput.svelte#L88-L118) use [BEM naming conventions](https://getbem.com/naming/). To override the default styles apply your styles as shown below:
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
- ```html
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canutin/svelte-currency-input",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "exports": {
5
5
  "./package.json": "./package.json",
6
6
  ".": "./index.js",