-
Notifications
You must be signed in to change notification settings - Fork 0
/
livenumberformat.js
390 lines (304 loc) · 13.4 KB
/
livenumberformat.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
export default class LiveNumberFormat {
constructor(el, options = {}) {
this.el = el;
this.undoStack = []; // Stack to maintain history for undo
this.redoStack = []; // Stack for redo
// Debounce timer to push to undo stack
this.debounceTimer = null;
this.debounceTime = options.debounceTime || 300;
this.allowNegative = options.allowNegative !== undefined ? options.allowNegative : true;
this.formatStyle = options.formatStyle !== undefined ? options.formatStyle : "thousand";
this.decimalMark = ".";
this.delimiter = ",";
this.decimalScale = options.decimalScale !== undefined ? options.decimalScale : Infinity;
this.integerScale = options.integerScale !== undefined ? options.integerScale : Infinity;
this.stripLeadingZeroes = options.stripLeadingZeroes !== undefined ? options.stripLeadingZeroes : false;
// Allow another decimal mark to replace the current decimal mark
this.allowDecimalReplacement = options.allowDecimalReplacement !== undefined ? options.allowDecimalReplacement : false;
this.maxUndoStackSize = options.maxUndoStackSize || 500;
this.handleInput = this.handleInput.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.backspacePressed = false;
this.isChangeFromUndoRedo = false;
this.init();
if (this.el.value) {
// Initial formatting, with cursor at the end
this.handleInput();
this.setCursorPosition(this.el.value.length);
}
}
init () {
this.el.addEventListener("input", this.handleInput);
this.el.addEventListener("keydown", this.handleKeydown);
}
// The following function of code is sourced from `cleave-zen`
// Licensed under the MIT License. See LICENSE in the project root for license information.
format (value) {
let parts, partSign, partInteger, partDecimal = '';
// Convert the formatted string to a number representation
value = value.replace(/[A-Za-z]/g, '')
// replace decimals with placeholder
.replace(this.decimalMark, 'D')
// strip non numeric letters except minus and decimal placeholder
.replace(/[^\dD-]/g, '')
// replace the leading minus with minus placeholder
.replace(/^\-/, 'M')
// replace any other minus sign
.replace(/\-/g, '')
// replace minus placeholder with minus sign
.replace('M', this.allowNegative ? '-' : '')
// replace decimal placeholder with decimal sign
.replace('D', this.decimalMark);
// strip any leading zeros, while keeping minus intact
if (this.stripLeadingZeroes) {
value = value.replace(/^(-)?0+(?=\d)/, '$1');
}
partSign = value.slice(0, 1) === '-' ? '-' : '';
parts = this.getParts(value, true, true);
partInteger = parts.partInteger;
// remove minus sign
if (partSign === '-') {
partInteger = parts.partInteger.slice(1);
}
// add decimal mark with the decimal value
if (value.includes(this.decimalMark)) {
partDecimal = this.decimalMark + parts.partDecimal;
}
switch (this.formatStyle) {
case "thousandLakhCrore":
partInteger = partInteger.replace(/(\d)(?=(\d\d)+\d$)/g, '$1' + this.delimiter);
break;
case "thousand":
partInteger = partInteger.replace(/(\d)(?=(\d{3})+$)/g, '$1' + this.delimiter);
break;
case "tenThousand":
partInteger = partInteger.replace(/(\d)(?=(\d{4})+$)/g, '$1' + this.delimiter);
break;
}
return partSign + partInteger.toString() + (this.decimalScale > 0 ? partDecimal.toString() : '');
}
handleInput (e) {
// Debounce the stack update for undo and redo
clearTimeout(this.debounceTimer);
let oldVal = this.el.value, cursorPosition = this.el.selectionEnd, newVal;
// if input has only delimiters and 0, do nothing
// e.g. 0,000,000,000 or ,000,000,000
if (!this.stripLeadingZeroes) {
if (oldVal.match(new RegExp(`^[${this.delimiter}0]*$`))) {
this.isChangeFromUndoRedo = false;
return;
}
}
newVal = this.format(this.el.value); // format value
// get the new cursor position
const newPosition = this.getNextCursorPosition(
cursorPosition,
oldVal,
newVal,
);
this.el.value = newVal;
this.setCursorPosition(newPosition);
// Push to stack only if the change is not from undo or redo
if (!this.isChangeFromUndoRedo) {
this.debounceTimer = setTimeout(() => {
this.pushToUndoStack({
"val": newVal,
"cpos": newPosition
});
}, this.debounceTime);
}
this.isChangeFromUndoRedo = false;
}
setCursorPosition (pos) {
pos = pos < 0 ? 0 : pos;
this.el.setSelectionRange(pos, pos);
}
getNextCursorPosition (prevPos, oldValue, newValue) {
// cursor already at the end, set it to the end of new value
if (oldValue.length === prevPos) {
return newValue.length;
}
return prevPos + this.getPositionOffset(prevPos, oldValue, newValue);
}
getDelimiterRegex () {
return new RegExp(this.delimiter, 'g');
}
// getParts returns the integer and decimal parts of the value
// clearDelimiters removes all delimiters from the value
// applyScale truncates the value to the integer and decimal scales
getParts (value, clearDelimiters = false, applyScale = false) {
let parts = value.split(this.decimalMark);
let partInteger = parts[0];
let partDecimal = '';
if (parts.length > 1) {
partDecimal = parts[1];
}
if (clearDelimiters) {
partInteger = partInteger.replace(this.getDelimiterRegex(), '');
partDecimal = partDecimal.replace(this.getDelimiterRegex(), '');
}
if (applyScale) {
partInteger = partInteger.slice(0, this.integerScale);
partDecimal = partDecimal.slice(0, this.decimalScale);
}
return {
partInteger,
partDecimal
};
}
getPositionOffset (prevPos, oldValue, newValue) {
// Edge case for negative values,
// e.g -2|,321,321 and user presses backspace.
if (oldValue.startsWith("-" + this.delimiter) && newValue.startsWith("-")) {
return 0;
}
let oldRawValue, newRawValue, newFormattedValueAfterCursor, oldFormattedValueAfterCursor;
oldFormattedValueAfterCursor = oldValue.slice(prevPos);
newFormattedValueAfterCursor = newValue.slice(prevPos);
oldRawValue = oldValue.slice(0, prevPos).replace(this.getDelimiterRegex(), '');
newRawValue = newValue.slice(0, prevPos).replace(this.getDelimiterRegex(), '');
// Replace all negatives. This is done to check if the user is adding a negative sign.
oldRawValue = oldRawValue.replace('-', '');
newRawValue = newRawValue.replace('-', '');
// Minus sign being added, since it was replaced in oldRawValue
// keep cursor at the same position
if (oldRawValue.includes("-")) {
return -1;
}
// Invalid characters entered, keep cursor at the same position
if (oldRawValue.match(/[^0-9\.-]/)) {
return -1;
}
// keep cursor at the same position
if (this.stripLeadingZeroes && oldRawValue === '0') {
return -1;
}
// delimeter position changed due to backspace, move cursor to its correct position
if (oldFormattedValueAfterCursor != newFormattedValueAfterCursor && this.backspacePressed) {
if (oldFormattedValueAfterCursor.startsWith(this.delimiter)) {
return -1;
}
}
// Direction of cursor movement. -1 is left, 1 is right, 0 is no movement
return Math.sign(oldRawValue.length - newRawValue.length);
}
handleKeydown (e) {
this.backspacePressed = false;
if (e.key === "Backspace") {
this.backspacePressed = true;
} else if (e.key === "ArrowLeft" && !e.shiftKey) {
this.handleLeftArrow(e);
return;
} else if (e.key === "ArrowRight" && !e.shiftKey) {
this.handleRightArrow(e);
return;
} else if (e.ctrlKey && (e.key === "z" || e.key === "Z")) {
this.performUndo(e);
return;
} else if (e.ctrlKey && (e.key === "y" || e.key === "Y")) {
this.performRedo(e);
return;
} else if (e.key === "Delete") {
// if cursor is before delimiter and is NOT at index 0, move cursor 1 place right
if (this.el.value[this.el.selectionStart] === this.delimiter && this.el.selectionStart != 0) {
e.preventDefault();
this.setCursorPosition(this.el.selectionStart + 1);
}
return;
}
// prevent typing another decimal mark or if the decimal scale is 0
if (e.key === this.decimalMark) {
if ((this.allowDecimalReplacement === false && this.el.value.includes(this.decimalMark)) || this.decimalScale === 0) {
e.preventDefault();
}
}
// For no range cursor selection, check if input is greater than the scales.
if (this.el.selectionStart === this.el.selectionEnd) {
// restrict input to integer and decimal scales
if (["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(e.key)) {
// Find the cursor position if on left or right of decimal mark
let parts = this.getParts(this.el.value, true);
let delimeterCount = this.el.value.split(this.delimiter).length - 1;
// subtract delimeter count from cursor position to get the correct position of cursor
let cursorPositionFromStart = this.el.selectionStart - delimeterCount;
// cursor is on left of decimal mark
if (cursorPositionFromStart <= parts.partInteger.length) {
if (parts.partInteger.length >= this.integerScale) {
e.preventDefault();
}
}
// cursor is on right of decimal mark
if (cursorPositionFromStart > parts.partInteger.length) {
if (parts.partDecimal.length >= this.decimalScale) {
e.preventDefault();
}
}
}
}
}
// handleLeftArrow moves cursor 2 places if previous character is delimiter
handleLeftArrow (e) {
const cursorPositionFromStart = this.el.selectionStart;
// if previous character is delimiter, move cursor 2 places
if (cursorPositionFromStart - 2 < 0) {
return;
}
if (this.el.value[cursorPositionFromStart - 2] === this.delimiter) {
e.preventDefault();
this.setCursorPosition(cursorPositionFromStart - 2);
}
}
// handleRightArrow moves cursor 2 places if next character is delimiter
handleRightArrow (e) {
const cursorPositionFromStart = this.el.selectionStart;
// if next character is delimiter, move cursor 2 places
if (this.el.value[cursorPositionFromStart] === this.delimiter) {
e.preventDefault();
this.setCursorPosition(cursorPositionFromStart + 2);
}
}
pushToUndoStack (value) {
if (this.undoStack.length <= this.maxUndoStackSize) {
this.undoStack.push(value);
this.redoStack = []; // Clear redo stack on new input
}
}
// performUndo does an undo operation, sets new value & adjusts cursor position
performUndo () {
this.isChangeFromUndoRedo = true;
if (this.undoStack.length > 1) {
let currentState = this.undoStack.pop();
this.redoStack.push(currentState);
let itemToSet = this.undoStack[this.undoStack.length - 1];
this.el.value = itemToSet.val;
this.setCursorPosition(itemToSet.cpos);
} else if (this.undoStack.length === 1) {
this.redoStack.push(this.undoStack.pop());
this.el.value = "";
}
}
// performRedo does a redo operation, sets new value & adjusts cursor position
performRedo () {
this.isChangeFromUndoRedo = true;
if (this.redoStack.length > 0) {
let itemToRedo = this.redoStack.pop();
this.undoStack.push(itemToRedo);
this.el.value = itemToRedo.val;
this.setCursorPosition(itemToRedo.cpos);
}
}
destroy () {
this.el.removeEventListener("input", this.handleInput);
this.el.removeEventListener("keydown", this.handleKeydown);
}
getRawValue () {
return this.el.value;
}
getFloatValue () {
let value = this.getRawValue();
if (value) {
value = value.match(/[0-9.-]+/g).join('');
}
return isNaN(parseFloat(value)) === true ? 0 : parseFloat(value);
}
}