|
|
@@ -21,6 +21,8 @@ public struct TextFieldWithToolBar: View {
|
|
|
|
|
|
@FocusState private var isFocused: Bool
|
|
|
@State private var localText: String = ""
|
|
|
+ // State flag to track if the field was intentionally cleared to zero
|
|
|
+ @State private var isZeroCleared: Bool = false
|
|
|
|
|
|
public init(
|
|
|
text: Binding<Decimal>,
|
|
|
@@ -71,6 +73,7 @@ public struct TextFieldWithToolBar: View {
|
|
|
Button(action: {
|
|
|
localText = ""
|
|
|
text = 0
|
|
|
+ isZeroCleared = true // Mark as cleared to prevent showing "0"
|
|
|
textDidChange?(0)
|
|
|
}) {
|
|
|
Image(systemName: "trash")
|
|
|
@@ -98,46 +101,137 @@ public struct TextFieldWithToolBar: View {
|
|
|
.onChange(of: isFocused) { _, newValue in
|
|
|
if newValue {
|
|
|
textFieldDidBeginEditing?()
|
|
|
+ // When gaining focus: if the value is zero and was previously cleared,
|
|
|
+ // keep the text field empty to show placeholder instead of "0"
|
|
|
+ if isZeroCleared, text == 0 {
|
|
|
+ localText = ""
|
|
|
+ }
|
|
|
} else {
|
|
|
- // Format when losing focus
|
|
|
- if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
- text = decimal
|
|
|
- localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
|
|
|
+ // When losing focus: handle formatting and validation
|
|
|
+ if localText.isEmpty {
|
|
|
+ // If field is empty, maintain zero value but mark as cleared
|
|
|
+ // so we can show placeholder instead of "0"
|
|
|
+ text = 0
|
|
|
+ isZeroCleared = true
|
|
|
+ } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
+ if decimal != 0 {
|
|
|
+ // For non-zero values, format normally and update binding
|
|
|
+ text = decimal
|
|
|
+ localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ } else {
|
|
|
+ // If user explicitly entered zero, store the value but
|
|
|
+ // keep display empty to show placeholder
|
|
|
+ text = 0
|
|
|
+ localText = ""
|
|
|
+ isZeroCleared = true
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- .onChange(of: localText) { _, newValue in
|
|
|
- handleTextChange(newValue)
|
|
|
+ .onChange(of: localText) { oldValue, newValue in
|
|
|
+ // Reset zero-cleared state as soon as user starts typing anything
|
|
|
+ if !newValue.isEmpty {
|
|
|
+ isZeroCleared = false
|
|
|
+ }
|
|
|
+
|
|
|
+ // Special handling for backspace operations to maintain decimal format
|
|
|
+ if oldValue.count == newValue.count + 1 {
|
|
|
+ let decimalSeparator = numberFormatter.decimalSeparator ?? "."
|
|
|
+
|
|
|
+ // Special case: When backspacing to leave only a decimal point
|
|
|
+ // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
|
|
|
+ if newValue.hasSuffix(decimalSeparator) {
|
|
|
+ if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
|
|
|
+ text = decimal
|
|
|
+ textDidChange?(decimal)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Special case: When backspacing the last digit after a decimal point
|
|
|
+ // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
|
|
|
+ if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
|
|
|
+ let oldParts = oldValue.components(separatedBy: decimalSeparator)
|
|
|
+ let newParts = newValue.components(separatedBy: decimalSeparator)
|
|
|
+
|
|
|
+ // Check if we've removed the last digit after decimal point
|
|
|
+ if oldParts.count > 1, newParts.count > 1,
|
|
|
+ oldParts[1].count == 1, newParts[1].isEmpty
|
|
|
+ {
|
|
|
+ // Keep proper decimal format by adding trailing zero
|
|
|
+ localText = newParts[0] + decimalSeparator + "0"
|
|
|
+
|
|
|
+ if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
+ text = decimal
|
|
|
+ textDidChange?(decimal)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Process normal text input changes
|
|
|
+ handleTextChange(oldValue, newValue)
|
|
|
+ }
|
|
|
+ .onChange(of: text) { oldValue, newValue in
|
|
|
+ // Handle external changes to the text binding
|
|
|
+ // (changes not initiated by typing, like programmatic changes)
|
|
|
+ if oldValue != newValue,
|
|
|
+ Decimal(string: localText, locale: numberFormatter.locale) != newValue
|
|
|
+ {
|
|
|
+ if newValue == 0, isZeroCleared {
|
|
|
+ // If value is zero and field was cleared, keep display empty to show placeholder
|
|
|
+ localText = ""
|
|
|
+ } else {
|
|
|
+ // Otherwise format and display the new value
|
|
|
+ localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
.onAppear {
|
|
|
if text != 0 {
|
|
|
+ // Initialize with formatted non-zero value
|
|
|
localText = numberFormatter.string(from: text as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ } else {
|
|
|
+ // For zero values, start with empty field to show placeholder
|
|
|
+ localText = ""
|
|
|
+ isZeroCleared = true
|
|
|
}
|
|
|
// Set initial focus if requested
|
|
|
isFocused = initialFocus
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func handleTextChange(_ newValue: String) {
|
|
|
- // Handle empty string
|
|
|
+ private func handleTextChange(_ oldValue: String, _ newValue: String) {
|
|
|
+ // Handle empty input (clear operation)
|
|
|
if newValue.isEmpty {
|
|
|
text = 0
|
|
|
+ isZeroCleared = true
|
|
|
textDidChange?(0)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // Remove leading zeros except for decimal values (e.g., "0.5")
|
|
|
+ // This prevents inputs like "01", "0123", etc. but allows "0.5"
|
|
|
+ if newValue.count > 1 && newValue.hasPrefix("0") && !newValue.hasPrefix("0" + (numberFormatter.decimalSeparator ?? ".")) {
|
|
|
+ localText = String(newValue.dropFirst())
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
let currentDecimalSeparator = numberFormatter.decimalSeparator ?? "."
|
|
|
|
|
|
- // Prevent multiple decimal separators
|
|
|
+ // Ensure there's only one decimal separator
|
|
|
let decimalSeparatorCount = newValue.filter { String($0) == currentDecimalSeparator }.count
|
|
|
if decimalSeparatorCount > 1 {
|
|
|
- // If there's already a decimal separator, prevent adding another one
|
|
|
- // by removing the last character (which would be the second decimal separator)
|
|
|
- localText = String(newValue.dropLast())
|
|
|
+ // Reject input with multiple decimal separators
|
|
|
+ localText = oldValue
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Replace wrong decimal separator with the correct one
|
|
|
+ // Handle localization by converting to the correct decimal separator
|
|
|
var processedText = newValue
|
|
|
if newValue.contains("."), currentDecimalSeparator != "." {
|
|
|
processedText = newValue.replacingOccurrences(of: ".", with: currentDecimalSeparator)
|
|
|
@@ -145,30 +239,59 @@ public struct TextFieldWithToolBar: View {
|
|
|
processedText = newValue.replacingOccurrences(of: ",", with: currentDecimalSeparator)
|
|
|
}
|
|
|
|
|
|
- // Handle leading decimal separator
|
|
|
+ // Automatically add leading zero when starting with decimal separator
|
|
|
+ // For example ".5" becomes "0.5"
|
|
|
if processedText.hasPrefix(currentDecimalSeparator) {
|
|
|
processedText = "0" + processedText
|
|
|
}
|
|
|
|
|
|
- // Update if valid decimal
|
|
|
+ // Validate against number formatter digit limits
|
|
|
+ let components = processedText.components(separatedBy: currentDecimalSeparator)
|
|
|
+
|
|
|
+ // Process the integer part (before decimal)
|
|
|
+ var integerPart = components[0].filter { $0.isNumber }
|
|
|
+ // Remove leading zeros for accurate digit counting
|
|
|
+ while integerPart.hasPrefix("0") && integerPart.count > 1 {
|
|
|
+ integerPart.removeFirst()
|
|
|
+ }
|
|
|
+ let integerDigits = integerPart.count
|
|
|
+
|
|
|
+ // Count fraction digits (after decimal separator)
|
|
|
+ let fractionDigits = components.count > 1 ? components[1].filter { $0.isNumber }.count : 0
|
|
|
+
|
|
|
+ // Validate against the formatter's digit limits
|
|
|
+ if integerDigits > numberFormatter.maximumIntegerDigits ||
|
|
|
+ (allowDecimalSeparator && fractionDigits > numberFormatter.maximumFractionDigits)
|
|
|
+ {
|
|
|
+ // Reject input that exceeds digit limits
|
|
|
+ localText = oldValue
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse and validate the decimal value
|
|
|
if let decimal = Decimal(string: processedText, locale: numberFormatter.locale) {
|
|
|
if let maxValue = maxValue, decimal > maxValue {
|
|
|
+ // Cap at maximum allowed value
|
|
|
text = maxValue
|
|
|
localText = numberFormatter.string(from: maxValue as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
} else {
|
|
|
+ // Accept valid input and update binding
|
|
|
text = decimal
|
|
|
+
|
|
|
+ // Update zero-cleared state based on the value
|
|
|
+ isZeroCleared = (decimal == 0) && localText.isEmpty
|
|
|
+
|
|
|
textDidChange?(decimal)
|
|
|
- }
|
|
|
-// text = decimal
|
|
|
-// textDidChange?(decimal)
|
|
|
|
|
|
- // If the processed text is different from the input, update the field
|
|
|
- if processedText != newValue {
|
|
|
- localText = processedText
|
|
|
+ // If we had to process/modify the input, update the displayed text
|
|
|
+ if processedText != newValue {
|
|
|
+ localText = processedText
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
- // If not a valid decimal, keep the old value
|
|
|
- localText = numberFormatter.string(from: text as NSNumber) ?? ""
|
|
|
+ // Reject invalid decimal inputs
|
|
|
+ localText = oldValue
|
|
|
}
|
|
|
}
|
|
|
}
|