formatEditUpdate method
Formats text input by filtering digits and managing cursor position
Called automatically by Flutter's text input system when user types.
Process:
- Extract only digits from new input
- Limit to 4 digits maximum
- Calculate appropriate cursor position
- Return formatted TextEditingValue
Cursor Logic:
- Counts difference in digit count between old and new values
- Adjusts cursor position based on this difference
- Clamps position to valid range
oldValue - Previous TextEditingValue
newValue - New TextEditingValue from user input
Returns filtered and formatted TextEditingValue
Implementation
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
var limitedDigits = _limitToFourDigits(
TimeInputControllers._removeNonDigits(newValue.text));
// In formatted mode, backspace over separators can otherwise remove the wrong digit.
// Enforce deletion semantics based on the caret slot in the old value.
bool isBackspace = false;
int backspaceSlot = 0;
if (_isCollapsedBackspace(oldValue, newValue)) {
final oldDigits = _limitToFourDigits(
TimeInputControllers._removeNonDigits(oldValue.text));
final removalIndex = _countDigitsBeforeOffset(
oldValue.text, oldValue.selection.baseOffset) -
1;
if (removalIndex >= 0 && removalIndex < oldDigits.length) {
// In full HHMM mode, backspace should clear the targeted digit slot
// while preserving structure (e.g. 13:45 -> 13:05 at minute-tens).
if (oldDigits.length >= 4) {
limitedDigits = _replaceDigitAt(oldDigits, removalIndex, '0');
} else {
limitedDigits = _removeDigitAt(oldDigits, removalIndex);
}
isBackspace = true;
backspaceSlot = removalIndex; // 0-based slot where deletion occurred
}
}
// Always keep the field displayed in formatted mode while typing.
final formattedText = TimeInputControllers.formatTimeInput(
limitedDigits,
isUtc: isUtc,
showLocalIndicator: showLocalIndicator,
);
// After backspace place caret BEFORE the digit at the freed slot so the
// cursor sits exactly where the deleted digit was (naturally an allowed offset).
// For all other edits, track by digit count as before.
final int newCursorPosition;
if (isBackspace) {
newCursorPosition = _positionAtDigitSlot(formattedText, backspaceSlot);
if (enableDebugLogs) {
assert(() {
debugPrint(
'[TI:formatter] BACKSPACE '
'old="${oldValue.text}" sel=${oldValue.selection.baseOffset} '
'-> digits="$limitedDigits" slot=$backspaceSlot '
'-> fmt="$formattedText" cur=$newCursorPosition',
);
return true;
}());
}
} else {
final digitsBeforeCursor = _countDigitsBeforeOffset(
newValue.text, newValue.selection.baseOffset);
newCursorPosition =
_positionAfterDigitCount(formattedText, digitsBeforeCursor);
if (enableDebugLogs) {
assert(() {
debugPrint(
'[TI:formatter] INSERT '
'new="${newValue.text}" sel=${newValue.selection.baseOffset} '
'-> digits="$limitedDigits" digitsBeforeCursor=$digitsBeforeCursor '
'-> fmt="$formattedText" cur=$newCursorPosition',
);
return true;
}());
}
}
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: newCursorPosition),
);
}