formatEditUpdate method
Called when text is being typed or cut/copy/pasted in the EditableText.
You can override the resulting text based on the previous text value and the incoming new text value.
When formatters are chained, oldValue
reflects the initial value of
TextEditingValue at the beginning of the chain.
Implementation
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (_leadingLength > 0 && _trailingLength > 0) {
throw 'You cannot use trailing an leading symbols at the same time';
}
var newText = newValue.text;
var oldText = oldValue.text;
if (oldValue == newValue) {
return newValue;
}
if (newText.contains(',.') || newText.contains('..')) {
/// this condition is processing a case when you press a period
/// after the cursor is already located in a mantissa part
return oldValue.copyWith(
selection: newValue.selection,
);
}
newText = _stripRepeatingSeparators(newText);
oldText = _stripRepeatingSeparators(oldText);
/// If a value starts with something like 02,000.50$
/// the zero, obviously, must be removed
int numZeroesRemovedAtStringStart = 0;
var newRemoveZeroResult = _removeWrongLeadingZero(
newText,
newValue,
);
if (newRemoveZeroResult != null) {
newText = newRemoveZeroResult;
numZeroesRemovedAtStringStart = 1;
}
var usesCommaForMantissa = _usesCommasForMantissa();
if (usesCommaForMantissa) {
newText = _swapCommasAndPeriods(newText);
oldText = _swapCommasAndPeriods(oldText);
oldValue = oldValue.copyWith(text: oldText);
newValue = newValue.copyWith(text: newText);
}
var usesSpacesAsThousandSeparator = _usesSpacesForThousands();
if (usesSpacesAsThousandSeparator) {
/// if spaces are used as thousand separators
/// they must be replaced with commas here
/// this is used to simplify value processing further
newText = _replaceSpacesByCommas(
newText,
leadingLength: _leadingLength,
trailingLength: _trailingLength,
);
oldText = _replaceSpacesByCommas(
oldText,
leadingLength: _leadingLength,
trailingLength: _trailingLength,
);
oldValue = oldValue.copyWith(text: oldText);
newValue = newValue.copyWith(text: newText);
}
var isErasing = newValue.text.length < oldValue.text.length;
TextSelection selection;
/// mantissa must always be a period here because the string at this
/// point is always formmated using commas as thousand separators
/// for simplicity
var mantissaSymbol = '.';
var leadingZeroWithDot = '${leadingSymbol}0$mantissaSymbol';
var leadingZeroWithoutDot = '$leadingSymbol$mantissaSymbol';
if (isErasing) {
if (newValue.selection.end < _leadingLength) {
selection = TextSelection.collapsed(
offset: _leadingLength,
);
return TextEditingValue(
selection: selection,
text: _prepareDotsAndCommas(oldText),
);
}
} else {
if (maxTextLength != null) {
if (newValue.text.length > maxTextLength!) {
/// we limit string length but only if it's the whole part
/// we should allow mantissa editing anyway
/// so this code restrictss the length only if we edit
/// the main part
var lastSeparatorIndex = oldText.lastIndexOf('.');
var isAfterMantissa = newValue.selection.end > lastSeparatorIndex + 1;
if (!newValue.text.contains('..')) {
if (!isAfterMantissa) {
return oldValue;
}
}
}
}
if (oldValue.text.length < 1 && newValue.text.length != 1) {
if (_leadingLength < 1) {
return newValue;
}
}
}
// if (newText.startsWith(leadingZeroWithoutDot)) {
// newText = newText.replaceFirst(leadingZeroWithoutDot, leadingZeroWithDot);
// }
_processCallback(newText);
if (isErasing) {
/// erases and reformats the whole string
selection = newValue.selection;
/// here we always have a fraction part
var lastSeparatorIndex = oldText.lastIndexOf('.');
if (selection.end == lastSeparatorIndex) {
/// if a caret was right after the mantissa separator then
/// we need to bring it before the separator
/// instead of erasing it
selection = TextSelection.collapsed(
offset: oldValue.selection.extentOffset - 1,
);
// print('OLD TEXT $oldText');
var preparedText = _prepareDotsAndCommas(oldText);
// print('PREPARED TEXT $preparedText');
return TextEditingValue(
selection: selection,
text: preparedText,
);
}
var isAfterSeparator = lastSeparatorIndex < selection.extentOffset;
if (isAfterSeparator && lastSeparatorIndex > -1) {
/// if the erasing started before the separator
/// allow erasing everything
return newValue.copyWith(
text: _prepareDotsAndCommas(newValue.text),
);
}
var numSeparatorsBefore = _countSymbolsInString(
newText,
',',
);
newText = toCurrencyString(
newText,
mantissaLength: mantissaLength,
leadingSymbol: leadingSymbol,
trailingSymbol: trailingSymbol,
thousandSeparator: ThousandSeparator.Comma,
useSymbolPadding: useSymbolPadding,
);
var numSeparatorsAfter = _countSymbolsInString(
newText,
',',
);
if (thousandSeparator == ThousandSeparator.None) {
/// in case the separator is None it will lead to the wrong
/// caret placement. Maybe this is not the best
/// solution to insert this code here, it's more like a dirty hack
/// but I haven't had time enough to think on some more sophisticated
/// architectural approach :D
numSeparatorsAfter = 0;
}
var selectionOffset = numSeparatorsAfter - numSeparatorsBefore;
int offset = selection.extentOffset + selectionOffset;
if (_leadingLength > 0) {
// _leadingLength = leadingSymbol.length;
if (offset < _leadingLength) {
offset += _leadingLength;
}
}
selection = TextSelection.collapsed(
offset: offset,
);
if (_leadingLength > 0) {
/// this code removes odd zeroes after a leading symbol
/// do NOT remove this code
if (newText.contains(leadingZeroWithDot)) {
newText = newText.replaceAll(
leadingZeroWithDot,
leadingZeroWithoutDot,
);
offset -= 1;
if (offset < _leadingLength) {
offset = _leadingLength;
}
selection = TextSelection.collapsed(
offset: offset,
);
}
}
var preparedText = _prepareDotsAndCommas(newText);
return TextEditingValue(
selection: selection,
text: preparedText,
);
}
/// stop isErasing
bool oldStartsWithLeading = leadingSymbol.isNotEmpty &&
oldValue.text.startsWith(
leadingSymbol,
);
/// count the number of thousand separators in an old string
/// then check how many of there are there in the new one and if
/// the number is different add this number to the selection offset
var oldSelectionEnd = oldValue.selection.end;
TextEditingValue value = oldSelectionEnd > -1 ? oldValue : newValue;
String oldSubstrBeforeSelection = oldSelectionEnd > -1
? value.text.substring(0, value.selection.end)
: '';
int numThousandSeparatorsInOldSub = _countSymbolsInString(
oldSubstrBeforeSelection,
',',
);
/// This check is necessary because if an input looks like this
/// $.5, toCurrencyString() method will convert it to
/// $0.5 and the selection must also be shifted by 1 symbol to the right
var startsWithOrphanPeriod = numericStringStartsWithOrphanPeriod(newText);
var formattedValue = toCurrencyString(
newText,
leadingSymbol: leadingSymbol,
mantissaLength: mantissaLength,
/// we always need a comma here because
/// this value is not final. The correct symbol will be
/// added in _prepareDotsAndCommas() method
thousandSeparator: ThousandSeparator.Comma,
trailingSymbol: trailingSymbol,
useSymbolPadding: useSymbolPadding,
);
/// this is the correctly formatted value
/// with commas as thousand separators like $1,500.00. The separator
/// replacements may occure below
// print(formattedValue);
String newSubstrBeforeSelection = oldSelectionEnd > -1
? formattedValue.substring(
0,
value.selection.end,
)
: '';
int numThousandSeparatorsInNewSub = _countSymbolsInString(
newSubstrBeforeSelection,
',',
);
int numAddedSeparators =
numThousandSeparatorsInNewSub - numThousandSeparatorsInOldSub;
if (thousandSeparator == ThousandSeparator.None) {
/// I really want to believe this :-)
numThousandSeparatorsInNewSub = 0;
numAddedSeparators = 0;
}
bool newStartsWithLeading = leadingSymbol.isNotEmpty &&
formattedValue.startsWith(
leadingSymbol,
);
/// if an old string did not contain a leading symbol but
/// the new one does then wee need to add a length of the leading
/// to the selection offset
bool addedLeading = !oldStartsWithLeading && newStartsWithLeading;
var selectionIndex = value.selection.end + numAddedSeparators;
int wholePartSubStart = 0;
if (addedLeading) {
wholePartSubStart = _leadingLength;
selectionIndex += _leadingLength;
}
if (startsWithOrphanPeriod) {
selectionIndex += 1;
}
/// The rare case when a string starts with 0 and no
/// mantissa separator after
selectionIndex -= numZeroesRemovedAtStringStart;
var mantissaIndex = formattedValue.indexOf(mantissaSymbol);
if (mantissaIndex > wholePartSubStart) {
var wholePartSubstring = formattedValue.substring(
wholePartSubStart,
mantissaIndex,
);
if (selectionIndex < mantissaIndex) {
if (wholePartSubstring == '0' ||
wholePartSubstring == '${leadingSymbol}0') {
/// if the whole part contains 0 only, then we need
/// to bring the selection after the
/// fractional part right away
selectionIndex += 1;
}
}
}
selectionIndex += 1;
if (oldValue.text.isEmpty && useSymbolPadding) {
/// to skip leading space right after a currency symbol
selectionIndex += 1;
}
var preparedText = _prepareDotsAndCommas(
formattedValue,
);
var selectionEnd = min(
selectionIndex,
preparedText.length,
);
return TextEditingValue(
selection: TextSelection.collapsed(
offset: selectionEnd,
),
text: preparedText,
);
}