@transferwise/components 0.0.0-experimental-adcaf57 → 0.0.0-experimental-2118ad7
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/build/index.esm.js +142 -68
- package/build/index.esm.js.map +1 -1
- package/build/index.js +143 -69
- package/build/index.js.map +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts.map +1 -1
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts +1 -2
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts.map +1 -1
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts +1 -5
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts.map +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts.map +1 -1
- package/build/types/index.d.ts +0 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts +11 -5
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/index.d.ts +1 -2
- package/build/types/inputWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/inputs/Input.d.ts +6 -8
- package/build/types/inputs/Input.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts +11 -5
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/index.d.ts +1 -2
- package/build/types/textareaWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts +82 -55
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/types/withDisplayFormat/index.d.ts +1 -2
- package/build/types/withDisplayFormat/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/textFormat/formatWithPattern/{formatWithPattern.ts → formatWithPattern.js} +4 -8
- package/src/common/textFormat/getCursorPositionAfterKeystroke/{getCursorPositionAfterKeystroke.ts → getCursorPositionAfterKeystroke.js} +6 -7
- package/src/common/textFormat/getSymbolsInPatternWithPosition/{getSymbolsInPatternWithPosition.ts → getSymbolsInPatternWithPosition.js} +2 -7
- package/src/common/textFormat/unformatWithPattern/{unformatWithPattern.ts → unformatWithPattern.js} +2 -3
- package/src/index.ts +0 -2
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.js +14 -0
- package/src/inputWithDisplayFormat/index.js +1 -0
- package/src/inputs/Input.tsx +11 -6
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.js +14 -0
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.spec.js +1 -3
- package/src/textareaWithDisplayFormat/index.js +1 -0
- package/src/withDisplayFormat/WithDisplayFormat.js +312 -0
- package/src/withDisplayFormat/index.js +1 -0
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.tsx +0 -11
- package/src/inputWithDisplayFormat/index.ts +0 -2
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +0 -30
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.tsx +0 -11
- package/src/textareaWithDisplayFormat/index.ts +0 -2
- package/src/withDisplayFormat/WithDisplayFormat.tsx +0 -303
- package/src/withDisplayFormat/index.ts +0 -2
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
|
|
4
|
+
import { HistoryNavigator } from '../common';
|
|
5
|
+
import {
|
|
6
|
+
formatWithPattern,
|
|
7
|
+
getCountOfSymbolsInSelection,
|
|
8
|
+
getCursorPositionAfterKeystroke,
|
|
9
|
+
unformatWithPattern,
|
|
10
|
+
getDistanceToPreviousSymbol,
|
|
11
|
+
getDistanceToNextSymbol,
|
|
12
|
+
} from '../common/textFormat';
|
|
13
|
+
|
|
14
|
+
class WithDisplayFormat extends Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
const { value, displayPattern } = props;
|
|
18
|
+
const unformattedValue = unformatWithPattern(value, displayPattern);
|
|
19
|
+
this.state = {
|
|
20
|
+
value: formatWithPattern(unformattedValue, displayPattern),
|
|
21
|
+
historyNavigator: new HistoryNavigator(),
|
|
22
|
+
prevDisplayPattern: props.displayPattern,
|
|
23
|
+
triggerType: null,
|
|
24
|
+
triggerEvent: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static getDerivedStateFromProps(nextProps, previousState) {
|
|
29
|
+
const { displayPattern } = nextProps;
|
|
30
|
+
const { prevDisplayPattern } = previousState;
|
|
31
|
+
if (previousState.prevDisplayPattern !== displayPattern) {
|
|
32
|
+
const { value, historyNavigator } = previousState;
|
|
33
|
+
|
|
34
|
+
const unFormattedValue = unformatWithPattern(value, prevDisplayPattern);
|
|
35
|
+
historyNavigator.reset();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
prevDisplayPattern: displayPattern,
|
|
39
|
+
value: formatWithPattern(unFormattedValue, displayPattern),
|
|
40
|
+
triggerType: null,
|
|
41
|
+
triggerEvent: null,
|
|
42
|
+
pastedLength: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getUserAction = (unformattedValue) => {
|
|
49
|
+
const { triggerEvent, triggerType, value } = this.state;
|
|
50
|
+
const { displayPattern } = this.props;
|
|
51
|
+
|
|
52
|
+
const charCode = String.fromCharCode(triggerEvent.which).toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (triggerType === 'Paste' || triggerType === 'Cut') {
|
|
55
|
+
return triggerType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ((triggerEvent.ctrlKey || triggerEvent.metaKey) && charCode === 'z') {
|
|
59
|
+
return triggerEvent.shiftKey ? 'Redo' : 'Undo';
|
|
60
|
+
}
|
|
61
|
+
// Detect mouse event redo
|
|
62
|
+
if (triggerEvent.ctrlKey && charCode === 'd') {
|
|
63
|
+
return 'Delete';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Android Fix.
|
|
67
|
+
if (typeof triggerEvent.key === 'undefined') {
|
|
68
|
+
if (unformattedValue.length <= unformatWithPattern(value, displayPattern).length) {
|
|
69
|
+
return 'Backspace';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return triggerEvent.key;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
resetEvent = () => {
|
|
77
|
+
this.setState({
|
|
78
|
+
triggerType: null,
|
|
79
|
+
triggerEvent: null,
|
|
80
|
+
pastedLength: 0,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
detectUndoRedo = (event) => {
|
|
85
|
+
const charCode = String.fromCharCode(event.which).toLowerCase();
|
|
86
|
+
if ((event.ctrlKey || event.metaKey) && charCode === 'z') {
|
|
87
|
+
return event.shiftKey ? 'Redo' : 'Undo';
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
handleOnKeyDown = (event) => {
|
|
93
|
+
event.persist();
|
|
94
|
+
const { selectionStart, selectionEnd } = event.target;
|
|
95
|
+
const { historyNavigator } = this.state;
|
|
96
|
+
const { displayPattern } = this.props;
|
|
97
|
+
|
|
98
|
+
// Unfortunately Undo and Redo don't trigger OnChange event so we need to handle some value logic here.
|
|
99
|
+
let newFormattedValue = '';
|
|
100
|
+
|
|
101
|
+
if (this.detectUndoRedo(event) === 'Undo') {
|
|
102
|
+
newFormattedValue = formatWithPattern(historyNavigator.undo(), displayPattern);
|
|
103
|
+
this.setState({ value: newFormattedValue, triggerType: 'Undo' });
|
|
104
|
+
} else if (this.detectUndoRedo(event) === 'Redo') {
|
|
105
|
+
newFormattedValue = formatWithPattern(historyNavigator.redo(), displayPattern);
|
|
106
|
+
this.setState({ value: newFormattedValue, triggerType: 'Redo' });
|
|
107
|
+
} else {
|
|
108
|
+
this.setState({
|
|
109
|
+
triggerEvent: event,
|
|
110
|
+
triggerType: 'KeyDown',
|
|
111
|
+
selectionStart,
|
|
112
|
+
selectionEnd,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
handleOnPaste = (event) => {
|
|
118
|
+
const { displayPattern } = this.props;
|
|
119
|
+
const pastedLength = unformatWithPattern(
|
|
120
|
+
event.clipboardData.getData('Text'),
|
|
121
|
+
displayPattern,
|
|
122
|
+
).length;
|
|
123
|
+
|
|
124
|
+
this.setState({ triggerType: 'Paste', pastedLength });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
handleOnCut = () => {
|
|
128
|
+
this.setState({ triggerType: 'Cut' });
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
isKeyAllowed = (action) => {
|
|
132
|
+
const { displayPattern } = this.props;
|
|
133
|
+
const symbolsInPattern = displayPattern.split('').filter((character) => character !== '*');
|
|
134
|
+
|
|
135
|
+
return !symbolsInPattern.includes(action);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
handleOnChange = (event) => {
|
|
139
|
+
const { historyNavigator, triggerEvent, triggerType } = this.state;
|
|
140
|
+
const { displayPattern, onChange } = this.props;
|
|
141
|
+
const { value } = event.target;
|
|
142
|
+
let unformattedValue = unformatWithPattern(value, displayPattern);
|
|
143
|
+
const action =
|
|
144
|
+
triggerEvent === null
|
|
145
|
+
? // triggerEvent can be null only in case of "autofilling" (via password manager extension or browser build-in one) events
|
|
146
|
+
'Paste'
|
|
147
|
+
: this.getUserAction(unformattedValue);
|
|
148
|
+
if (!this.isKeyAllowed(action) || triggerType === 'Undo' || triggerType === 'Redo') {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action === 'Backspace' || action === 'Delete') {
|
|
153
|
+
unformattedValue = this.handleDelete(unformattedValue, action);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const newFormattedValue = formatWithPattern(unformattedValue, displayPattern);
|
|
157
|
+
historyNavigator.add(unformattedValue);
|
|
158
|
+
|
|
159
|
+
this.handleCursorPositioning(action);
|
|
160
|
+
|
|
161
|
+
const broadcastValue = unformatWithPattern(newFormattedValue, displayPattern);
|
|
162
|
+
|
|
163
|
+
this.setState({ value: newFormattedValue }, this.resetEvent(), onChange(broadcastValue));
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
handleOnBlur = (event) => {
|
|
167
|
+
const { displayPattern, onBlur } = this.props;
|
|
168
|
+
if (onBlur) {
|
|
169
|
+
onBlur(unformatWithPattern(event.target.value, displayPattern));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
handleOnFocus = (event) => {
|
|
174
|
+
const { displayPattern, onFocus } = this.props;
|
|
175
|
+
if (onFocus) {
|
|
176
|
+
this.handleOnChange(event);
|
|
177
|
+
onFocus(unformatWithPattern(event.target.value, displayPattern));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
handleDelete = (unformattedValue, action) => {
|
|
182
|
+
const { displayPattern } = this.props;
|
|
183
|
+
const { selectionStart, selectionEnd } = this.state;
|
|
184
|
+
const newStack = [...unformattedValue];
|
|
185
|
+
if (selectionStart === selectionEnd) {
|
|
186
|
+
let startPosition =
|
|
187
|
+
selectionStart - getCountOfSymbolsInSelection(0, selectionStart, displayPattern);
|
|
188
|
+
let toDelete = 0;
|
|
189
|
+
|
|
190
|
+
let count = getDistanceToNextSymbol(selectionStart, displayPattern);
|
|
191
|
+
if (action === 'Backspace') {
|
|
192
|
+
startPosition -= 1;
|
|
193
|
+
count = getDistanceToPreviousSymbol(selectionStart, displayPattern);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (startPosition >= 0 && count) {
|
|
197
|
+
toDelete = 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
newStack.splice(startPosition, toDelete);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return newStack.join('');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
handleCursorPositioning = (action) => {
|
|
207
|
+
const { displayPattern } = this.props;
|
|
208
|
+
const { triggerEvent, selectionStart, selectionEnd, pastedLength } = this.state;
|
|
209
|
+
|
|
210
|
+
const cursorPosition = getCursorPositionAfterKeystroke(
|
|
211
|
+
action,
|
|
212
|
+
selectionStart,
|
|
213
|
+
selectionEnd,
|
|
214
|
+
displayPattern,
|
|
215
|
+
pastedLength,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (triggerEvent) {
|
|
220
|
+
triggerEvent.target.setSelectionRange(cursorPosition, cursorPosition);
|
|
221
|
+
}
|
|
222
|
+
this.setState({ selectionStart: cursorPosition, selectionEnd: cursorPosition });
|
|
223
|
+
}, 0);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
render() {
|
|
227
|
+
const {
|
|
228
|
+
type,
|
|
229
|
+
inputMode,
|
|
230
|
+
className,
|
|
231
|
+
id,
|
|
232
|
+
name,
|
|
233
|
+
placeholder,
|
|
234
|
+
readOnly,
|
|
235
|
+
required,
|
|
236
|
+
minLength,
|
|
237
|
+
maxLength,
|
|
238
|
+
disabled,
|
|
239
|
+
autoComplete,
|
|
240
|
+
} = this.props;
|
|
241
|
+
const { value } = this.state;
|
|
242
|
+
const renderProps = {
|
|
243
|
+
type,
|
|
244
|
+
inputMode,
|
|
245
|
+
className,
|
|
246
|
+
id,
|
|
247
|
+
name,
|
|
248
|
+
placeholder,
|
|
249
|
+
readOnly,
|
|
250
|
+
required,
|
|
251
|
+
minLength,
|
|
252
|
+
maxLength,
|
|
253
|
+
disabled,
|
|
254
|
+
autoComplete,
|
|
255
|
+
value,
|
|
256
|
+
onFocus: this.handleOnFocus,
|
|
257
|
+
onBlur: this.handleOnBlur,
|
|
258
|
+
onPaste: this.handleOnPaste,
|
|
259
|
+
onKeyDown: this.handleOnKeyDown,
|
|
260
|
+
onChange: this.handleOnChange,
|
|
261
|
+
onCut: this.handleOnCut,
|
|
262
|
+
};
|
|
263
|
+
return this.props.render(renderProps);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
WithDisplayFormat.propTypes = {
|
|
268
|
+
/**
|
|
269
|
+
* autocomplete hides our form help so we need to disable it when help text
|
|
270
|
+
* is present. Chrome ignores autocomplete=off, the only way to disable it is
|
|
271
|
+
* to provide an 'invalid' value, for which 'disabled' serves.
|
|
272
|
+
*/
|
|
273
|
+
autoComplete: PropTypes.oneOf(['on', 'off', 'disabled']),
|
|
274
|
+
className: PropTypes.string,
|
|
275
|
+
disabled: PropTypes.bool,
|
|
276
|
+
id: PropTypes.string,
|
|
277
|
+
maxLength: PropTypes.number,
|
|
278
|
+
minLength: PropTypes.number,
|
|
279
|
+
name: PropTypes.string,
|
|
280
|
+
onFocus: PropTypes.func,
|
|
281
|
+
onBlur: PropTypes.func,
|
|
282
|
+
onChange: PropTypes.func.isRequired,
|
|
283
|
+
placeholder: PropTypes.string,
|
|
284
|
+
readOnly: PropTypes.bool,
|
|
285
|
+
render: PropTypes.func.isRequired,
|
|
286
|
+
required: PropTypes.bool,
|
|
287
|
+
displayPattern: PropTypes.string,
|
|
288
|
+
type: PropTypes.string,
|
|
289
|
+
inputMode: PropTypes.string,
|
|
290
|
+
value: PropTypes.string,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
WithDisplayFormat.defaultProps = {
|
|
294
|
+
autoComplete: 'off',
|
|
295
|
+
className: null,
|
|
296
|
+
disabled: false,
|
|
297
|
+
id: null,
|
|
298
|
+
maxLength: null,
|
|
299
|
+
minLength: null,
|
|
300
|
+
name: null,
|
|
301
|
+
placeholder: null,
|
|
302
|
+
readOnly: false,
|
|
303
|
+
required: false,
|
|
304
|
+
displayPattern: '',
|
|
305
|
+
type: 'text',
|
|
306
|
+
inputMode: null,
|
|
307
|
+
value: '',
|
|
308
|
+
onFocus: null,
|
|
309
|
+
onBlur: null,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default WithDisplayFormat;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './WithDisplayFormat';
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Input, type InputProps } from '../inputs/Input';
|
|
2
|
-
import WithDisplayFormat, { type TextInputProps } from '../withDisplayFormat';
|
|
3
|
-
|
|
4
|
-
const InputWithDisplayFormat = (props: TextInputProps) => (
|
|
5
|
-
<WithDisplayFormat
|
|
6
|
-
{...props}
|
|
7
|
-
render={(renderProps) => <Input {...(renderProps as InputProps)} />}
|
|
8
|
-
/>
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
export default InputWithDisplayFormat;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { StoryObj } from '@storybook/react';
|
|
2
|
-
|
|
3
|
-
import { userEvent, within } from '../test-utils';
|
|
4
|
-
|
|
5
|
-
import TextareaWithDisplayFormat from './TextareaWithDisplayFormat';
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
component: TextareaWithDisplayFormat,
|
|
9
|
-
title: 'Forms/TextareaWithDisplayFormat',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type Story = StoryObj<typeof TextareaWithDisplayFormat>;
|
|
13
|
-
|
|
14
|
-
export const Basic: Story = {
|
|
15
|
-
render: (args) => {
|
|
16
|
-
return (
|
|
17
|
-
<>
|
|
18
|
-
<TextareaWithDisplayFormat
|
|
19
|
-
value="0000"
|
|
20
|
-
displayPattern="**** - **** - ****"
|
|
21
|
-
onChange={console.log}
|
|
22
|
-
/>
|
|
23
|
-
</>
|
|
24
|
-
);
|
|
25
|
-
},
|
|
26
|
-
play: ({ canvasElement }) => {
|
|
27
|
-
const canvas = within(canvasElement);
|
|
28
|
-
userEvent.type(canvas.getByRole('textbox'), '111122223333');
|
|
29
|
-
},
|
|
30
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { TextArea, type TextAreaProps } from '../inputs/TextArea';
|
|
2
|
-
import WithDisplayFormat, { type TextInputProps } from '../withDisplayFormat';
|
|
3
|
-
|
|
4
|
-
const TextareaWithDisplayFormat = (props: TextInputProps) => (
|
|
5
|
-
<WithDisplayFormat
|
|
6
|
-
{...props}
|
|
7
|
-
render={(renderProps) => <TextArea {...(renderProps as TextAreaProps)} />}
|
|
8
|
-
/>
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
export default TextareaWithDisplayFormat;
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { Component, KeyboardEvent, ClipboardEvent, ChangeEvent, FocusEvent } from 'react';
|
|
2
|
-
|
|
3
|
-
import { HistoryNavigator } from '../common';
|
|
4
|
-
import {
|
|
5
|
-
formatWithPattern,
|
|
6
|
-
getCountOfSymbolsInSelection,
|
|
7
|
-
getCursorPositionAfterKeystroke,
|
|
8
|
-
unformatWithPattern,
|
|
9
|
-
getDistanceToPreviousSymbol,
|
|
10
|
-
getDistanceToNextSymbol,
|
|
11
|
-
} from '../common/textFormat';
|
|
12
|
-
import { InputProps } from '../inputs/Input';
|
|
13
|
-
import { TextAreaProps } from '../inputs/TextArea';
|
|
14
|
-
import { Merge } from '../utils';
|
|
15
|
-
|
|
16
|
-
export type TextInputProps = Merge<
|
|
17
|
-
InputProps,
|
|
18
|
-
{
|
|
19
|
-
value?: string;
|
|
20
|
-
displayPattern: string;
|
|
21
|
-
/**
|
|
22
|
-
* autocomplete hides our form help so we need to disable it when help text
|
|
23
|
-
* is present. Chrome ignores autocomplete=off, the only way to disable it is
|
|
24
|
-
* to provide an 'invalid' value, for which 'disabled' serves.
|
|
25
|
-
*/
|
|
26
|
-
autoComplete?: InputProps['autoComplete'] | 'disabled';
|
|
27
|
-
onChange?: (value: string) => void;
|
|
28
|
-
onBlur?: (value: string) => void;
|
|
29
|
-
onFocus?: (value: string) => void;
|
|
30
|
-
}
|
|
31
|
-
>;
|
|
32
|
-
|
|
33
|
-
export type Props = typeof WithDisplayFormat.defaultProps & {
|
|
34
|
-
render: (renderProps: InputProps | TextAreaProps) => JSX.Element;
|
|
35
|
-
} & TextInputProps;
|
|
36
|
-
|
|
37
|
-
export type EventType =
|
|
38
|
-
| 'KeyDown'
|
|
39
|
-
| 'Paste'
|
|
40
|
-
| 'Cut'
|
|
41
|
-
| 'Undo'
|
|
42
|
-
| 'Redo'
|
|
43
|
-
| 'Backspace'
|
|
44
|
-
| 'Delete'
|
|
45
|
-
| null;
|
|
46
|
-
|
|
47
|
-
type State = {
|
|
48
|
-
value: string;
|
|
49
|
-
historyNavigator: HistoryNavigator;
|
|
50
|
-
prevDisplayPattern?: string;
|
|
51
|
-
triggerType?: EventType;
|
|
52
|
-
triggerEvent?: KeyboardEvent<HTMLInputElement> | null;
|
|
53
|
-
pastedLength?: number;
|
|
54
|
-
selectionStart?: HTMLInputElement['selectionStart'];
|
|
55
|
-
selectionEnd?: HTMLInputElement['selectionEnd'];
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
class WithDisplayFormat extends Component<Props, State> {
|
|
59
|
-
static defaultProps = {
|
|
60
|
-
autoComplete: 'off',
|
|
61
|
-
displayPattern: '',
|
|
62
|
-
type: 'text',
|
|
63
|
-
value: '',
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
constructor(props: Props) {
|
|
67
|
-
super(props);
|
|
68
|
-
const unformattedValue = unformatWithPattern(props.value, props.displayPattern);
|
|
69
|
-
this.state = {
|
|
70
|
-
value: formatWithPattern(unformattedValue, props.displayPattern),
|
|
71
|
-
historyNavigator: new HistoryNavigator(),
|
|
72
|
-
prevDisplayPattern: props.displayPattern,
|
|
73
|
-
triggerType: null,
|
|
74
|
-
triggerEvent: null,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
static getDerivedStateFromProps(
|
|
79
|
-
{ displayPattern }: Props,
|
|
80
|
-
{ prevDisplayPattern = displayPattern, value, historyNavigator }: State,
|
|
81
|
-
) {
|
|
82
|
-
if (prevDisplayPattern !== displayPattern) {
|
|
83
|
-
const unFormattedValue = unformatWithPattern(value, prevDisplayPattern);
|
|
84
|
-
historyNavigator.reset();
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
prevDisplayPattern: displayPattern,
|
|
88
|
-
value: formatWithPattern(unFormattedValue, displayPattern),
|
|
89
|
-
triggerType: null,
|
|
90
|
-
triggerEvent: null,
|
|
91
|
-
pastedLength: 0,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
getUserAction = (unformattedValue: string): EventType => {
|
|
98
|
-
const { triggerEvent, triggerType, value } = this.state;
|
|
99
|
-
const { displayPattern } = this.props;
|
|
100
|
-
|
|
101
|
-
if (triggerEvent) {
|
|
102
|
-
const charCode = String.fromCharCode(triggerEvent.which).toLowerCase();
|
|
103
|
-
|
|
104
|
-
if (triggerType === 'Paste' || triggerType === 'Cut') {
|
|
105
|
-
return triggerType;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if ((triggerEvent.ctrlKey || triggerEvent.metaKey) && charCode === 'z') {
|
|
109
|
-
return triggerEvent.shiftKey ? 'Redo' : 'Undo';
|
|
110
|
-
}
|
|
111
|
-
// Detect mouse event redo
|
|
112
|
-
if (triggerEvent.ctrlKey && charCode === 'd') {
|
|
113
|
-
return 'Delete';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Android Fix.
|
|
117
|
-
if (
|
|
118
|
-
typeof triggerEvent.key === 'undefined' &&
|
|
119
|
-
unformattedValue.length <= unformatWithPattern(value, displayPattern).length
|
|
120
|
-
) {
|
|
121
|
-
return 'Backspace';
|
|
122
|
-
}
|
|
123
|
-
return triggerEvent.key as EventType;
|
|
124
|
-
} else {
|
|
125
|
-
// triggerEvent can be null only in case of "autofilling" (via password manager extension or browser build-in one) events
|
|
126
|
-
return 'Paste';
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
resetEvent = () => {
|
|
131
|
-
this.setState({
|
|
132
|
-
triggerType: null,
|
|
133
|
-
triggerEvent: null,
|
|
134
|
-
pastedLength: 0,
|
|
135
|
-
});
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
detectUndoRedo = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
139
|
-
const charCode = String.fromCharCode(event.which).toLowerCase();
|
|
140
|
-
if ((event.ctrlKey || event.metaKey) && charCode === 'z') {
|
|
141
|
-
return event.shiftKey ? 'Redo' : 'Undo';
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
handleOnKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
147
|
-
event.persist();
|
|
148
|
-
const { selectionStart, selectionEnd } = event.target as HTMLInputElement;
|
|
149
|
-
const { historyNavigator } = this.state;
|
|
150
|
-
const { displayPattern } = this.props;
|
|
151
|
-
|
|
152
|
-
// Unfortunately Undo and Redo don't trigger OnChange event so we need to handle some value logic here.
|
|
153
|
-
let newFormattedValue = '';
|
|
154
|
-
|
|
155
|
-
if (this.detectUndoRedo(event) === 'Undo') {
|
|
156
|
-
newFormattedValue = formatWithPattern(historyNavigator.undo(), displayPattern);
|
|
157
|
-
this.setState({ value: newFormattedValue, triggerType: 'Undo' });
|
|
158
|
-
} else if (this.detectUndoRedo(event) === 'Redo') {
|
|
159
|
-
newFormattedValue = formatWithPattern(historyNavigator.redo(), displayPattern);
|
|
160
|
-
this.setState({ value: newFormattedValue, triggerType: 'Redo' });
|
|
161
|
-
} else {
|
|
162
|
-
this.setState({
|
|
163
|
-
triggerEvent: event,
|
|
164
|
-
triggerType: 'KeyDown',
|
|
165
|
-
selectionStart,
|
|
166
|
-
selectionEnd,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
handleOnPaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
|
172
|
-
const { displayPattern } = this.props;
|
|
173
|
-
const pastedLength = unformatWithPattern(
|
|
174
|
-
event.clipboardData.getData('Text'),
|
|
175
|
-
displayPattern,
|
|
176
|
-
).length;
|
|
177
|
-
|
|
178
|
-
this.setState({ triggerType: 'Paste', pastedLength });
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
handleOnCut = () => {
|
|
182
|
-
this.setState({ triggerType: 'Cut' });
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
isKeyAllowed = (action: EventType) => {
|
|
186
|
-
const { displayPattern } = this.props;
|
|
187
|
-
const symbolsInPattern = displayPattern.split('').filter((character) => character !== '*');
|
|
188
|
-
|
|
189
|
-
return !symbolsInPattern.includes(action as string);
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
193
|
-
const { historyNavigator, triggerType } = this.state;
|
|
194
|
-
const { displayPattern, onChange } = this.props;
|
|
195
|
-
const { value } = event.target;
|
|
196
|
-
let unformattedValue = unformatWithPattern(value, displayPattern);
|
|
197
|
-
const action = this.getUserAction(unformattedValue);
|
|
198
|
-
if (!this.isKeyAllowed(action) || triggerType === 'Undo' || triggerType === 'Redo') {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (action === 'Backspace' || action === 'Delete') {
|
|
203
|
-
unformattedValue = this.handleDelete(unformattedValue, action);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const newFormattedValue = formatWithPattern(unformattedValue, displayPattern);
|
|
207
|
-
historyNavigator.add(unformattedValue);
|
|
208
|
-
|
|
209
|
-
this.handleCursorPositioning(action);
|
|
210
|
-
|
|
211
|
-
this.setState({ value: newFormattedValue }, () => {
|
|
212
|
-
this.resetEvent();
|
|
213
|
-
if (onChange) {
|
|
214
|
-
const broadcastValue = unformatWithPattern(newFormattedValue, displayPattern);
|
|
215
|
-
onChange(broadcastValue);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
handleOnBlur = (event: FocusEvent<HTMLInputElement>) => {
|
|
221
|
-
this.props.onBlur?.(unformatWithPattern(event.target.value, this.props.displayPattern));
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
handleOnFocus = (event: FocusEvent<HTMLInputElement>) => {
|
|
225
|
-
const { displayPattern, onFocus } = this.props;
|
|
226
|
-
if (onFocus) {
|
|
227
|
-
this.handleOnChange(event);
|
|
228
|
-
onFocus(unformatWithPattern(event.target.value, displayPattern));
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
handleDelete = (unformattedValue: string, action: EventType) => {
|
|
233
|
-
const { displayPattern } = this.props;
|
|
234
|
-
const { selectionStart, selectionEnd } = this.state;
|
|
235
|
-
const newStack = [...unformattedValue];
|
|
236
|
-
if (selectionStart === selectionEnd) {
|
|
237
|
-
let startPosition =
|
|
238
|
-
(selectionStart as number) -
|
|
239
|
-
getCountOfSymbolsInSelection(0, selectionStart, displayPattern);
|
|
240
|
-
let toDelete = 0;
|
|
241
|
-
|
|
242
|
-
let count = getDistanceToNextSymbol(selectionStart, displayPattern);
|
|
243
|
-
if (action === 'Backspace') {
|
|
244
|
-
startPosition -= 1;
|
|
245
|
-
count = getDistanceToPreviousSymbol(selectionStart, displayPattern);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (startPosition >= 0 && count) {
|
|
249
|
-
toDelete = 1;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
newStack.splice(startPosition, toDelete);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return newStack.join('');
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
handleCursorPositioning = (action: EventType) => {
|
|
259
|
-
const { displayPattern } = this.props;
|
|
260
|
-
const { triggerEvent, selectionStart, selectionEnd, pastedLength } = this.state;
|
|
261
|
-
|
|
262
|
-
const cursorPosition = getCursorPositionAfterKeystroke(
|
|
263
|
-
action,
|
|
264
|
-
selectionStart as number,
|
|
265
|
-
selectionEnd as number,
|
|
266
|
-
displayPattern,
|
|
267
|
-
pastedLength as number,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
setTimeout(() => {
|
|
271
|
-
if (triggerEvent) {
|
|
272
|
-
(triggerEvent.target as HTMLInputElement).setSelectionRange(cursorPosition, cursorPosition);
|
|
273
|
-
}
|
|
274
|
-
this.setState({ selectionStart: cursorPosition, selectionEnd: cursorPosition });
|
|
275
|
-
}, 0);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
render() {
|
|
279
|
-
return this.props.render({
|
|
280
|
-
value: this.state.value,
|
|
281
|
-
type: this.props.type,
|
|
282
|
-
inputMode: this.props.inputMode,
|
|
283
|
-
className: this.props.className,
|
|
284
|
-
id: this.props.id,
|
|
285
|
-
name: this.props.name,
|
|
286
|
-
placeholder: this.props.placeholder,
|
|
287
|
-
readOnly: this.props.readOnly,
|
|
288
|
-
required: this.props.required,
|
|
289
|
-
minLength: this.props.minLength,
|
|
290
|
-
maxLength: this.props.maxLength,
|
|
291
|
-
disabled: this.props.disabled,
|
|
292
|
-
autoComplete: this.props.autoComplete,
|
|
293
|
-
onFocus: this.handleOnFocus,
|
|
294
|
-
onBlur: this.handleOnBlur,
|
|
295
|
-
onPaste: this.handleOnPaste,
|
|
296
|
-
onKeyDown: this.handleOnKeyDown,
|
|
297
|
-
onChange: this.handleOnChange,
|
|
298
|
-
onCut: this.handleOnCut,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export default WithDisplayFormat;
|