formatEditUpdate method

  1. @override
TextEditingValue formatEditUpdate(
  1. TextEditingValue oldValue,
  2. TextEditingValue newValue
)
override

Formats text input by filtering digits and managing cursor position

Called automatically by Flutter's text input system when user types.

Process:

  1. Extract only digits from new input
  2. Limit to 4 digits maximum
  3. Calculate appropriate cursor position
  4. 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),
  );
}