Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
Expand All @@ -86,26 +86,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
Expand Down Expand Up @@ -195,18 +195,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.6"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
Expand All @@ -216,5 +216,5 @@ packages:
source: hosted
version: "14.3.1"
sdks:
dart: ">=3.7.0-0 <4.0.0"
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
158 changes: 146 additions & 12 deletions lib/src/amount_input_formatter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,38 @@ class AmountInputFormatter extends TextInputFormatter {
required TextEditingValue newValue,
required String newText,
}) {
// Special case: Handle when user types "-" on zero value
// The formatted value will be "-0" (or "-0.000"), cursor should be after "-0"
final oldTextStartsWithMinus = oldValue.text.startsWith('-');
final newTextStartsWithMinus = newText.startsWith('-');
if (formatter.doubleValue == 0 &&
newTextStartsWithMinus &&
!oldTextStartsWithMinus &&
(oldValue.text == '0' ||
oldValue.text.isEmpty ||
oldValue.text.replaceAll(RegExp('[^0-9.]'), '').replaceAll('.', '') == '0')) {
// User typed "-" on zero, position cursor after "-0" (at indexOfDot)
return formatter.indexOfDot;
}

// Special case: Handle when user types a digit after "-0"
// Transition from "-0" to negative number (e.g., "-1")
// Check if we're transitioning from zero to a small negative number
final isTypingDigitAfterNegativeZero = formatter.previousValue == 0 &&
formatter.doubleValue < 0 &&
oldTextStartsWithMinus &&
newTextStartsWithMinus &&
formatter.doubleValue.abs() <= 9;

if (isTypingDigitAfterNegativeZero) {
// User typed a digit after "-0", position cursor after the digit (at indexOfDot)
// This prevents cursor from jumping to decimal separator
return formatter.indexOfDot;
}

// Assuming that it is the start of the input set the selection to the end
// of the integer part.
if (oldValue.selection.baseOffset <= 1 && formatter.doubleValue <= 9) {
if (oldValue.selection.baseOffset <= 1 && formatter.doubleValue.abs() <= 9) {
if (newText.isEmpty) return 0;

if (formatter.doubleValue == 0) {
Expand All @@ -100,22 +129,26 @@ class AmountInputFormatter extends TextInputFormatter {

// Assuming that it is the start of the input set the selection to the
// end of the integer part.
// Handle small numbers (including negative) when cursor is at start
if (oldValue.selection.baseOffset <= 1 &&
formatter.doubleValue <= 9 &&
formatter.previousValue <= 9) {
formatter.doubleValue.abs() <= 9 &&
formatter.previousValue.abs() <= 9) {
return formatter.indexOfDot;
}

// Case if the overall text length didn't change
// (i.e. one character replacement).
if (oldValue.text.length == newText.length) {
// Special case: If we're typing a digit after "-0", use indexOfDot instead of newSelection
if (isTypingDigitAfterNegativeZero) {
return formatter.indexOfDot;
}

final oldSelection = oldValue.selection;
final newSelection = newValue.selection;

if (newSelection.baseOffset > oldSelection.baseOffset) {
return newSelection.baseOffset > newText.length
? newText.length
: newSelection.baseOffset;
return newSelection.baseOffset > newText.length ? newText.length : newSelection.baseOffset;
}

return newSelection.baseOffset;
Expand All @@ -126,15 +159,13 @@ class AmountInputFormatter extends TextInputFormatter {

if (newText.length < oldValue.text.length) {
offset = oldValue.text.length - newText.length > 1
? oldValue.selection.baseOffset -
(oldValue.text.length - newText.length)
? oldValue.selection.baseOffset - (oldValue.text.length - newText.length)
: oldValue.selection.baseOffset - 1;
return offset < 0 ? 0 : offset;
}

offset = newText.length - oldValue.text.length > 1
? oldValue.selection.baseOffset +
(newText.length - oldValue.text.length)
? oldValue.selection.baseOffset + (newText.length - oldValue.text.length)
: oldValue.selection.baseOffset + 1;

return offset > newText.length ? newText.length - 1 : offset;
Expand All @@ -145,6 +176,108 @@ class AmountInputFormatter extends TextInputFormatter {
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Special case: Handle deletion right after decimal separator
// When cursor is right after the decimal separator and user hits delete,
// we should remove the digit before the decimal separator instead of removing the separator itself
final decimalSeparator = formatter.dcSeparator;
final oldText = oldValue.text;
final oldSelection = oldValue.selection;
final newTextInput = newValue.text;

// Check if we're deleting (text got shorter)
final isDeleting = newTextInput.length < oldText.length;

// Check if old text has decimal separator
final oldHasDecimalSeparator = oldText.contains(decimalSeparator);

// Check if cursor was right after decimal separator in old value
// We need to find the position of decimal separator in old text
// Use lastIndexOf in case there are multiple occurrences (unlikely but possible)
final decimalSeparatorIndex = oldHasDecimalSeparator ? oldText.lastIndexOf(decimalSeparator) : -1;
// Check if cursor is right after the decimal separator (delete key deletes the character before cursor)
final oldCursorOffset = oldSelection.baseOffset;
final cursorWasAfterDecimalSeparator = decimalSeparatorIndex >= 0 &&
oldCursorOffset == decimalSeparatorIndex + 1;

// Check if new text doesn't have decimal separator but old text did
final newHasDecimalSeparator = newTextInput.contains(decimalSeparator);
final decimalSeparatorWasRemoved = oldHasDecimalSeparator && !newHasDecimalSeparator;

// If all conditions are met, we need to modify the deletion behavior
if (isDeleting && cursorWasAfterDecimalSeparator && decimalSeparatorWasRemoved) {
// Instead of removing the decimal separator, we should remove the last digit
// from the integer part before the decimal separator
// Work directly with the old text string to preserve exact digits
final groupSeparator = formatter.intSeparator;

// Find the decimal separator position in the old text
if (decimalSeparatorIndex > 0) {
// Extract the integer part (everything before the decimal separator)
// Remove group separators to get just the digits
final integerPartWithSeparators = oldText.substring(0, decimalSeparatorIndex);
final integerPartDigits = integerPartWithSeparators.replaceAll(groupSeparator, '').replaceAll(RegExp(r'[^\d\-]'), '');

// Check if we have a negative sign
final isNegative = integerPartDigits.startsWith('-');
final integerDigitsOnly = isNegative ? integerPartDigits.substring(1) : integerPartDigits;

// Remove the last digit from the integer part
if (integerDigitsOnly.length > 1) {
final newIntegerDigits = integerDigitsOnly.substring(0, integerDigitsOnly.length - 1);

// Get the decimal part from the old text (everything after the decimal separator)
final decimalPart = oldText.substring(decimalSeparatorIndex + 1);
// Remove any non-digit characters from decimal part (like group separators, though unlikely)
final decimalDigitsOnly = decimalPart.replaceAll(RegExp(r'[^\d]'), '');

// Reconstruct the number string with the sign
final newNumericString = isNegative
? '-$newIntegerDigits$decimalSeparator$decimalDigitsOnly'
: '$newIntegerDigits$decimalSeparator$decimalDigitsOnly';

// Process the modified text
final processedText = formatter.processTextValue(
textInput: newNumericString,
);

if (processedText == null) return oldValue;

// Calculate cursor position - should be before the decimal separator
final newDecimalSeparatorIndex = processedText.indexOf(decimalSeparator);
final cursorOffset = newDecimalSeparatorIndex >= 0 ? newDecimalSeparatorIndex : processedText.length;

return TextEditingValue(
text: processedText,
selection: TextSelection.collapsed(offset: cursorOffset),
);
} else if (integerDigitsOnly.length == 1 && integerDigitsOnly != '0') {
// If integer part has only one non-zero digit, removing it makes it "0"
final decimalPart = oldText.substring(decimalSeparatorIndex + 1);
final decimalDigitsOnly = decimalPart.replaceAll(RegExp(r'[^\d]'), '');

final newNumericString = isNegative
? '-0$decimalSeparator$decimalDigitsOnly'
: '0$decimalSeparator$decimalDigitsOnly';

final processedText = formatter.processTextValue(
textInput: newNumericString,
);

if (processedText == null) return oldValue;

final newDecimalSeparatorIndex = processedText.indexOf(decimalSeparator);
final cursorOffset = newDecimalSeparatorIndex >= 0 ? newDecimalSeparatorIndex : processedText.length;

return TextEditingValue(
text: processedText,
selection: TextSelection.collapsed(offset: cursorOffset),
);
}
// If integer part is "0", we can't remove a digit, so just position cursor before decimal separator
// This case will fall through to normal processing, but cursor positioning should be handled
}
}

final newText = formatter.processTextValue(
textInput: newValue.text,
);
Expand Down Expand Up @@ -179,10 +312,11 @@ class AmountInputFormatter extends TextInputFormatter {
num number, {
TextEditingController? attachedController,
}) {
if (attachedController == null) return formatter.setNumValue(number);
final formattedText = formatter.setNumValue(number);
if (attachedController == null) return formattedText;

attachedController.value = TextEditingValue(
text: formatter.setNumValue(number),
text: formattedText,
selection: TextSelection.collapsed(offset: formatter.indexOfDot),
);

Expand Down
Loading