formatEditUpdate method

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

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,
  );
}