Search code examples
stringjavafxformatphone-numberformatter

How to format a phone number in a textfield


I'm looking to format a phone number in real time as it is typed into a TextField. The goal is to add a "separator" (a space, a dash, a dot...) between the different digits forming the phone number according to the country. By default France : 06 23 65 14 85 But also for other countries as the international format French : +33 6 23 65 14 85 Or the german international format : +xx xxx xxx xx xxx

For this, I have a Listener that looks permanently when a new number is added in the TextField. The program then takes care of detecting the format of the phone number that is being entered, and depending on that, the program uses a "format" that I created myself to modify the format of the string.

The problem is that the user's cursor is constantly moving while reformatting the string. If the user deletes or selects a zone and then deletes it... each time, the cursor moves to the beginning or the end of the string which is very annoying to write.

Would you have a solution to change the format of the string in real time while making sure that the cursor is in the right place? The only alternative I found for the moment is a lib but that seems to be difficult to maintain so I'd rather do it myself. Otherwise I have to wait for the unfocus on the graphic component and change the format after that, but that's not what I want.

I provide you my code below, excuse me if there is a little bit of French in it, I sometimes write some.

import java.util.function.UnaryOperator;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.util.converter.IntegerStringConverter;

public class FormPhoneNumber {

    private StringProperty textPropertyFromComponent = new SimpleStringProperty();
    private TextField tf;
    private String separator;
    private String last = "";
    private int lastCaretPosition = 0;
    private ChangeListener<String> updateText;

    public FormPhoneNumber(TextField textField, String separator) {
        this.textPropertyFromComponent = textField.textProperty();
        this.tf = textField;


//On unfocus
//        tf.focusedProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
//            if (t1 == false) {
//                System.out.println("i");
//            }
//        });
        
        if (separator != null && !separator.isEmpty()) {
            this.separator = separator;
        } else {
            separator = " ";
        }
        checkNumberFormat();
    }

    private void checkNumberFormat() {

//Listener on write in the textfield
        updateText = (ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
            if (newValue == null || newValue.isEmpty()) {
                return;
            }

//My customs formatters
            if (newValue.startsWith("+33")) {
//                System.out.println("FRANCE");
                String resultat = getPhoneNumberFormatted(newValue, "###-#-##-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 17);
            } else if (newValue.startsWith("+49")) {
//                System.out.println("ALLEMAGNE");
                String resultat = getPhoneNumberFormatted(newValue, "###-###-###-##-###");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 18);
            } else if (newValue.startsWith("+34")) {
//                System.out.println("ESPAGNE");
                String resultat = getPhoneNumberFormatted(newValue, "###-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.replaceAll(separator, "").startsWith("+3245") || newValue.replaceAll(separator, "").startsWith("+3249")) {
//                System.out.println("BELGIQUE GSM");
                String resultat = getPhoneNumberFormatted(newValue, "###-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.replaceAll(separator, "").startsWith("+322") || newValue.replaceAll(separator, "").startsWith("+323") || newValue.replaceAll(separator, "").startsWith("+329")) {
//                System.out.println("BELGIQUE AUTRE 1");
                String resultat = getPhoneNumberFormatted(newValue, "###-#-###-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 15);
            } else if (newValue.startsWith("+32")) {
//                System.out.println("BELGIQUE");
                String resultat = getPhoneNumberFormatted(newValue, "###-##-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 15);
            } else if (newValue.replaceAll(separator, "").startsWith("+3526")) {
//                System.out.println("LUXEMBOURG 1");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-###-###");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+352")) {
//                System.out.println("LUXEMBOURG AUTRE");
                String resultat = getPhoneNumberFormatted(newValue, "####-##-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+377")) {
//                System.out.println("MONACO");
                String resultat = getPhoneNumberFormatted(newValue, "####-##-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+41")) {
//                System.out.println("SUISSE");
                String resultat = getPhoneNumberFormatted(newValue, "###-##-###-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.replaceAll(separator, "").startsWith("+390")) {
//                System.out.println("ITALIE FIXE");
                String resultat = getPhoneNumberFormatted(newValue, "###-##-####-####");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.replaceAll(separator, "").startsWith("+393")) {
//                System.out.println("ITALIE PORTABLE");
                String resultat = getPhoneNumberFormatted(newValue, "###-###-###-####");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+39")) {
//                System.out.println("ITALIE AUTRE");
                String resultat = getPhoneNumberFormatted(newValue, "###-##-####-####");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+376")) {
//                System.out.println("ANDORRE");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-###-###");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 16);
            } else if (newValue.startsWith("+590")) {
//                System.out.println("GUADELOUPE");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 17);
            } else if (newValue.startsWith("+596")) {
//                System.out.println("MARTINIQUE");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 17);
            } else if (newValue.startsWith("+594")) {
//                System.out.println("GUYANE");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 17);
            } else if (newValue.startsWith("+262")) {
//                System.out.println("REUNION ET MAYOTTE");
                String resultat = getPhoneNumberFormatted(newValue, "####-###-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 17);
            } else if (newValue.startsWith("+687")) {
//                System.out.println("NOUVELLE-CALEDONIE");
                String resultat = getPhoneNumberFormatted(newValue, "####-##-##-##");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 13);
            } else if (newValue.startsWith("+")) {
//                System.out.println("AUTRE - PAYS NON REFERENCE");
                String resultat = getPhoneNumberFormatted(newValue, "######################");
                int caretPosition = tf.getCaretPosition();
                setResult(resultat, caretPosition, oldValue, newValue, 22);
            } else if (newValue.length() >= 3) {
                if (Character.isDigit(newValue.charAt(0))) {
//                    System.out.println("France Nationnal");
                    String resultat = getPhoneNumberFormatted(newValue, "##-##-##-##-##");
                    int caretPosition = tf.getCaretPosition();
                    setResult(resultat, caretPosition, oldValue, newValue, 14);
                }
            }
        };
        textPropertyFromComponent.addListener(updateText);
    }

//My attempt to put the cursor in the right place, but VERY scary code
    private void setResult(String resultat, int caretPosition, String oldValue, String newValue, int maxLength) {
        Platform.runLater(() -> {
            if (resultat.length() <= maxLength && newValue != null && oldValue != null) {
                textPropertyFromComponent.set(resultat);
//                int diffAddLetter = newValue.length() - oldValue.length();
//                int diffRemLetter = oldValue.length() - newValue.length();
//                System.out.println("oldValue = " + oldValue);
//                System.out.println("newValue = " + newValue);
//                System.out.println("resultat = " + resultat);
//                System.out.println("test = " + last);
//                if (newValue.length() == maxLength) {
//                    if (!"".equals(last)) {
//                        tf.positionCaret(caretPosition + 1);
//                    }
//                } else if (diffAddLetter > 0) {
//                    System.out.println("diffAddLetter = " + diffAddLetter);
//                    System.out.println("lastCaretPosition = " + lastCaretPosition);
//                    System.out.println("caretPosition = " + caretPosition);
//                    if ("".equals(last)) {
//                        if (StringUtils.countMatches(oldValue, separator) < StringUtils.countMatches(newValue, separator)) {
//                            tf.positionCaret(caretPosition + 1);
//                        } else {
//                            tf.positionCaret(caretPosition);
//                        }
//                        System.out.println("a1");
//                    } else {
//                        tf.positionCaret(caretPosition + diffAddLetter);
//                        System.out.println("a2");
//                    }
//                    if (lastCaretPosition == caretPosition) {
//                        last = "";
//                    } else {
//                        last = oldValue;
//                    }
//                } else if (diffRemLetter > 0) {
//                    System.out.println("lastCaretPosition = " + lastCaretPosition);
//                    System.out.println("caretPosition = " + caretPosition);
//                    System.out.println("diffRemLetter = " + diffRemLetter);
//                    if ((lastCaretPosition == caretPosition || lastCaretPosition == (caretPosition - diffRemLetter)) && diffRemLetter > 1) {
//                        tf.positionCaret(caretPosition - diffRemLetter);
//                        System.out.println("r1");
//                    } else {
//                        System.out.println("caretPosition + diffRemLetter = " + (caretPosition - diffRemLetter));
////                        System.out.println("(newValue.substring(0, caretPosition) = " + (newValue.substring(0, caretPosition)));
//                        System.out.println("oldValue.substring(0, caretPosition) = " + oldValue.substring(0, caretPosition));
////                        System.out.println("newValue.substring(caretPosition) = " + newValue.substring(caretPosition +1));
//                        System.out.println("caretPosition = " + caretPosition);
//                        if (newValue.length() > caretPosition) {
//                            if (newValue.substring(0, caretPosition).equals(oldValue.substring(0, caretPosition)) && oldValue.endsWith(newValue.substring(caretPosition + 1)) && !"".equals(newValue.substring(caretPosition + 1)) && !String.valueOf(newValue.charAt(caretPosition - 1)).equals(separator)) {
//                                tf.positionCaret(caretPosition);
//                                System.out.println("r2");
//                            } else if (Character.isDigit(newValue.charAt(caretPosition - 1)) && Character.isDigit(newValue.charAt(caretPosition)) && diffRemLetter == 1) {
//                                tf.positionCaret(caretPosition);
//                                System.out.println("r5");
//                            } else {
//                                tf.positionCaret(caretPosition + 1);
//                                System.out.println("r3");
//                            }
//                        } else {
//                            System.out.println("r4");
//                            tf.positionCaret(lastCaretPosition);
//                        }
//                        last = oldValue;
//                    }
//                } else if (String.valueOf(resultat.charAt(caretPosition - 1)).equals(separator) && last.length() < resultat.length()) {
//                    tf.positionCaret(caretPosition + 1);
//                    System.out.println("s");
//                } else {
//                    tf.positionCaret(caretPosition);
//                    System.out.println("o");
//                }
//                if (resultat.length() >= caretPosition) {
//                    if (oldValue.replaceAll(separator, "").equals(newValue.replaceAll(separator, "")) && String.valueOf(resultat.charAt(caretPosition)).equals(separator)) {
//                        tf.positionCaret(caretPosition);
//                        System.out.println("c");
//                    }
//                }
//                lastCaretPosition = caretPosition;
            } else {
                textPropertyFromComponent.set(oldValue);
            }
        });
        textPropertyFromComponent.addListener(updateText);
    }

//Method to format the phone number
    private String getPhoneNumberFormatted(String phoneNumber, String format) {
        textPropertyFromComponent.removeListener(updateText);
        String vretour = "";
        if (phoneNumber.length() <= format.length()) {
            phoneNumber = phoneNumber.replaceAll("[^0-9+]", "");
            int index = findXDotPosition(format, phoneNumber.length());
            format = format.substring(0, index + 1);
            int indexCharArray = 0;
            for (char unChar : format.toCharArray()) {
                if ("#".charAt(0) == unChar) {
                    vretour = vretour + phoneNumber.toCharArray()[indexCharArray];
                    indexCharArray++;
                }
                if ("-".charAt(0) == unChar) {
                    vretour = vretour + separator;
                }
            }
        } else {
            vretour = phoneNumber;
        }
        return vretour;
    }

//Method to find a separator
    private static int findXDotPosition(String s, int separatorToFind) {
        int result = -1;
        char[] ca = s.toCharArray();
        for (int i = 0; i < ca.length; ++i) {
            if (ca[i] == '#') {
                --separatorToFind;
            }
            if (separatorToFind == 0) {
                return i;
            }
        }
        return result;
    }
}

I also provide you some screenshots of the actual working of the application.

https://i.goopics.net/Jl8aR.png https://i.goopics.net/RVvdZ.png

Hoping you can offer me a solution, Thanks in advance !


Solution

  • TextFormatter is definitely the way to go. What you need to build is the filter component for the TextFormatter. Basically, you can think of this as a keystroke processor (that handles mouse actions as well), where all of the low-level stuff has been handled for you. You get a TextFormatter.Change object passed to you, and you can see how that change will impact the value in the field, and then modify it, suppress it, or let it pass through.

    So all of the formatting happens instantly, right in the TextField as you type.

    I built one to handle North American style phone numbers, because it's a little more interesting than the European style since it has brackets and dashes. But you can adapt it.

    The approach I took was to strip all of the formatting characters out of the string, then reformat it from scratch each time a change was made. This seems easier than trying to fiddle with it character by character. The only tricky part was hitting <BackSpace> over a "-" or ")", where I assumed that you'd want to delete the number in front of the special character. It might make more sense to just move the caret to the left of the special character:

    public class PhoneNumberFilter implements UnaryOperator<TextFormatter.Change> {
    
    @Override
    public TextFormatter.Change apply(TextFormatter.Change change) {
        if (change.isContentChange()) {
            handleBackspaceOverSpecialCharacter(change);
            if (change.getText().matches("[0-9]*")) {
                int originalNewTextLength = change.getControlNewText().length();
                change.setText(formatNumber(change.getControlNewText()));
                change.setRange(0, change.getControlText().length());
                int caretOffset = change.getControlNewText().length() - originalNewTextLength;
                change.setCaretPosition(change.getCaretPosition() + caretOffset);
                change.setAnchor(change.getAnchor() + caretOffset);
                return change;
            } else {
                return null;
            }
        }
        return change;
    }
    
    private void handleBackspaceOverSpecialCharacter(TextFormatter.Change change) {
        if (change.isDeleted() && (change.getSelection().getLength() == 0)) {
            if (!Character.isDigit(change.getControlText().charAt(change.getRangeStart()))) {
                if (change.getRangeStart() > 0) {
                    change.setRange(change.getRangeStart() - 1, change.getRangeEnd() - 1);
                }
            }
        }
    }
    
    private String formatNumber(String numbers) {
        numbers = numbers.replaceAll("[^\\d]", "");
        numbers = numbers.substring(0, Math.min(10, numbers.length()));
        if (numbers.length() == 0) {
            return "";
        }
        if (numbers.length() < 4) {
            return "(" + numbers;
        }
        if (numbers.length() < 7) {
            return numbers.replaceFirst("(\\d{3})(\\d+)", "($1)$2");
        }
        return numbers.replaceFirst("(\\d{3})(\\d{3})(\\d+)", "($1)$2-$3");
    }
    

    }

    Here's a little test application. You can see that the converter is just the "out of the box" default converter, since it's just a string composed of mostly numbers:

    public class PhoneNumberTest extends Application {
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) {
            TextField textField = new TextField();
            TextFormatter<String> textFormatter = new TextFormatter(new DefaultStringConverter(), "", new PhoneNumberFilter());
            textField.setTextFormatter(textFormatter);
            Scene scene = new Scene(new VBox(textField), 300, 100);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    }