From 351d8f0c74b2887693033a63bc8f92ef1bf3af3f Mon Sep 17 00:00:00 2001 From: Gabriel Diaz Date: Wed, 25 Oct 2023 16:17:46 -0500 Subject: [PATCH] Added strict mode to avoid inserting more chars than the mask --- .../dlsc/gemsfx/demo/PhoneNumberFieldApp.java | 35 +++- .../com/dlsc/gemsfx/PhoneNumberField.java | 198 +++++++++++------- 2 files changed, 145 insertions(+), 88 deletions(-) diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PhoneNumberFieldApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PhoneNumberFieldApp.java index 9e909367..2a75d66e 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PhoneNumberFieldApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PhoneNumberFieldApp.java @@ -34,7 +34,6 @@ public class PhoneNumberFieldApp extends Application { @Override public void start(Stage stage) throws Exception { PhoneNumberField field = new PhoneNumberField(); - field.setPhoneNumber("573003767182"); VBox controls = new VBox(10); addControl("Available Countries", availableCountriesSelector(field), controls); @@ -42,6 +41,8 @@ public void start(Stage stage) throws Exception { addControl("Default Country", defaultCountrySelector(field), controls); addControl("Disable Country", disableCountryCheck(field), controls); addControl("Force Local Phone", forceLocalPhoneNumberCheck(field), controls); + addControl("Strict Mode", strictModeCheck(field), controls); + addControl("Unmasked", unmaskedModeCheck(field), controls); VBox fields = new VBox(10); addField(fields, "Country Code", field.countryCallingCodeProperty(), COUNTRY_CODE_CONVERTER); @@ -56,7 +57,7 @@ public void start(Stage stage) throws Exception { vBox.getChildren().addAll(controls, new Separator(), field, new Separator(), fields); stage.setTitle("PhoneNumberField"); - stage.setScene(new Scene(vBox, 500, 450)); + stage.setScene(new Scene(vBox, 500, 500)); stage.sizeToScene(); stage.centerOnScreen(); stage.show(); @@ -106,15 +107,33 @@ private Node defaultCountrySelector(PhoneNumberField view) { } private Node disableCountryCheck(PhoneNumberField field) { - CheckBox localCheck = new CheckBox(); - localCheck.selectedProperty().bindBidirectional(field.disableCountryCodeProperty()); - return localCheck; + CheckBox check = new CheckBox(); + check.selectedProperty().bindBidirectional(field.disableCountryCodeProperty()); + return check; } private Node forceLocalPhoneNumberCheck(PhoneNumberField field) { - CheckBox localCheck = new CheckBox(); - localCheck.selectedProperty().bindBidirectional(field.forceLocalNumberProperty()); - return localCheck; + CheckBox check = new CheckBox(); + check.selectedProperty().bindBidirectional(field.forceLocalNumberProperty()); + return check; + } + + private Node strictModeCheck(PhoneNumberField field) { + CheckBox check = new CheckBox(); + check.selectedProperty().bindBidirectional(field.strictModeProperty()); + return check; + } + + private Node unmaskedModeCheck(PhoneNumberField field) { + CheckBox check = new CheckBox(); + check.selectedProperty().addListener((obs, oldV, newV) -> { + if (newV) { + field.setMaskProvider(null); + } else { + field.setMaskProvider(PhoneNumberField.DEFAULT_MASK_PROVIDER); + } + }); + return check; } private void addControl(String name, Node control, VBox controls) { diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PhoneNumberField.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PhoneNumberField.java index 7b0cacfa..5aa1df17 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PhoneNumberField.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PhoneNumberField.java @@ -58,6 +58,13 @@ public class PhoneNumberField extends Control { */ public static final char DEFAULT_MASK_DIGIT_CHAR = '_'; + public static final Callback DEFAULT_MASK_PROVIDER = field -> { + if (field.isForceLocalNumber()) { + return DEFAULT_MASK; + } + return Optional.ofNullable(field.getCountryCallingCode()).map(CountryCallingCode::localNumberMask).orElse(""); + }; + /** * List of supported characters a mask can have. */ @@ -71,16 +78,11 @@ public class PhoneNumberField extends Control { * {@link CountryCallingCode.Defaults Defaults}. */ public PhoneNumberField() { - getStyleClass().add(DEFAULT_STYLE_CLASS); - getAvailableCountryCodes().setAll(CountryCallingCode.Defaults.values()); - setMaskProvider(code -> { - if (isForceLocalNumber()) { - return DEFAULT_MASK; - } - return Optional.ofNullable(code).map(CountryCallingCode::localNumberMask).orElse(""); - }); parser = new PhoneNumberParser(); formatter = new PhoneNumberFormatter(); + getStyleClass().add(DEFAULT_STYLE_CLASS); + getAvailableCountryCodes().setAll(CountryCallingCode.Defaults.values()); + setMaskProvider(DEFAULT_MASK_PROVIDER); } @Override @@ -117,7 +119,7 @@ public void set(String newPhoneNumber) { // No need to infer the country code, just use the local phone number setCountryCallingCode(null); setLocalPhoneNumber(newPhoneNumber); - formatter.updateFormattedLocalPhoneNumber(newPhoneNumber); + formatter.setFormattedLocalPhoneNumber(newPhoneNumber); return; } @@ -127,11 +129,11 @@ public void set(String newPhoneNumber) { if (parsedNumber == null) { setCountryCallingCode(null); setLocalPhoneNumber(null); - formatter.updateFormattedLocalPhoneNumber(null); + formatter.setFormattedLocalPhoneNumber(null); } else { setCountryCallingCode(parsedNumber.countryCallingCode); setLocalPhoneNumber(parsedNumber.localPhoneNumber); - formatter.updateFormattedLocalPhoneNumber(parsedNumber.localPhoneNumber); + formatter.setFormattedLocalPhoneNumber(parsedNumber.localPhoneNumber); } } finally { @@ -171,7 +173,7 @@ public void set(CountryCallingCode newCountryCallingCode) { super.set(newCountryCallingCode); // Set the mask first - setMask(getMaskProvider().call(newCountryCallingCode)); + setMask(Optional.ofNullable(getMaskProvider()).map(p -> p.call(PhoneNumberField.this)).orElse(null)); // For now replace the entire text, it might be good to preserve the local number and just change the country code if (isForceLocalNumber()) { @@ -235,6 +237,10 @@ public final String getFormattedLocalPhoneNumber() { return formattedLocalPhoneNumber.get(); } + private void setFormattedLocalPhoneNumber(String formattedLocalPhoneNumber) { + this.formattedLocalPhoneNumber.set(formattedLocalPhoneNumber); + } + // SETTINGS private final ObservableList availableCountryCodes = FXCollections.observableArrayList(); @@ -332,7 +338,21 @@ public final void setCountryCodeViewFactory(Callback c countryCodeViewFactoryProperty().set(countryCodeViewFactory); } - private final ReadOnlyStringWrapper mask = new ReadOnlyStringWrapper(this, "mask"); + private final ReadOnlyStringWrapper mask = new ReadOnlyStringWrapper(this, "mask") { + @Override + public void set(String newMask) { + if (newMask != null && !newMask.isEmpty()) { + for (char c : newMask.toCharArray()) { + if (!DEFAULT_MASK_SUPPORTED_CHARS.contains(c)) { + throw new IllegalArgumentException("Mask contains unsupported character: " + c); + } + } + } + + super.set(newMask); + formatter.setFormattedLocalPhoneNumber(getLocalPhoneNumber()); + } + }; /** * @return The mask the control will use to format phone numbers. @@ -349,7 +369,7 @@ private void setMask(String mask) { this.mask.set(mask); } - private final ReadOnlyStringWrapper maskRemaining = new ReadOnlyStringWrapper(this, "maskRemaining"); + private final ReadOnlyStringWrapper maskRemaining = new ReadOnlyStringWrapper(this, "maskRemaining", getMask()); /** * @return A property that allows to know whether the mask is completed or not. @@ -366,28 +386,47 @@ private void setMaskRemaining(String mask) { this.maskRemaining.set(mask); } - private final ObjectProperty> maskProvider = new SimpleObjectProperty<>(this, "maskProvider") { + private final ObjectProperty> maskProvider = new SimpleObjectProperty<>(this, "maskProvider") { @Override - public void set(Callback maskProvider) { - super.set(Objects.requireNonNull(maskProvider)); + public void set(Callback maskProvider) { + super.set(maskProvider); + setMask(Optional.ofNullable(maskProvider).map(p -> p.call(PhoneNumberField.this)).orElse(null)); } }; /** - * @return The mask provider used to determine the mask for a given country calling code. + * @return The mask provider used to determine the mask. */ - public final ObjectProperty> maskProviderProperty() { + public final ObjectProperty> maskProviderProperty() { return maskProvider; } - public final Callback getMaskProvider() { + public final Callback getMaskProvider() { return maskProviderProperty().get(); } - public final void setMaskProvider(Callback maskProvider) { + public final void setMaskProvider(Callback maskProvider) { maskProviderProperty().set(maskProvider); } + private final BooleanProperty strictMode = new SimpleBooleanProperty(this, "strictMode"); + + /** + * @return Flag that defines the control to strictly follow the mask or not. If set to true, the control will not allow to enter + * numbers beyond the mask definition. If set to false, you can enter any number of digits even thought they overrun the mask. + */ + public final BooleanProperty strictModeProperty() { + return strictMode; + } + + public final boolean isStrictMode() { + return strictModeProperty().get(); + } + + public final void setStrictMode(boolean strictMode) { + strictModeProperty().set(strictMode); + } + /** * Represents a country calling code. The country calling code is used to identify the country and the area code. This ones * should go according the ITU-T E.164 recommendation. @@ -818,86 +857,85 @@ private final class PhoneNumberFormatter implements UnaryOperator(this)); - formattedLocalPhoneNumber.bind(textField.textProperty()); - maskProperty().addListener(obs -> compileMask(getMask())); - compileMask(getMask()); + formattedLocalPhoneNumber.bindBidirectional(textField.textProperty()); } - @Override - public TextFormatter.Change apply(TextFormatter.Change change) { + private void setFormattedLocalPhoneNumber(String newPhoneNumber) { if (selfUpdate) { - return change; + // Ignore when I'm the one who initiated the update + return; } try { selfUpdate = true; - - if (getCountryCallingCode() == null && !isForceLocalNumber()) { - return null; - } - - if (change.isAdded() || change.isDeleted() || change.isReplaced()) { - if (change.isAdded() || change.isReplaced()) { - String text = change.getText(); - if (!text.matches("[0-9]+")) { - return null; - } - } - - } - - if (change.isAdded()) { - change = numbersAdded(change); - } else if (change.isReplaced()) { - return null; - } else if (change.isDeleted()) { - change = numbersRemoved(change); - } - + String formattedLocalPhoneNumber = doFormat(newPhoneNumber); + PhoneNumberField.this.setFormattedLocalPhoneNumber(formattedLocalPhoneNumber); + updateMaskRemaining(formattedLocalPhoneNumber); } finally { selfUpdate = false; } + } - - return change; + private boolean isUnMasked() { + return getMask() == null || getMask().isEmpty(); } - private void updateFormattedLocalPhoneNumber(String newPhoneNumber) { + @Override + public TextFormatter.Change apply(TextFormatter.Change change) { if (selfUpdate) { - // Ignore when I'm the one who initiated the update - return; + return change; } try { selfUpdate = true; - String formattedLocalPhoneNumber = doFormat(newPhoneNumber); - textField.setText(formattedLocalPhoneNumber); - updateMaskRemaining(formattedLocalPhoneNumber); - } finally { - selfUpdate = false; - } - } - private void compileMask(String mask) { - if (mask != null && !mask.isEmpty()) { - for (char c : mask.toCharArray()) { - if (!DEFAULT_MASK_SUPPORTED_CHARS.contains(c)) { - throw new IllegalArgumentException("Mask contains unsupported character: " + c); + if (change.isAdded() || change.isReplaced()) { + String text = change.getText(); + if (!text.matches("[0-9]+")) { + return null; } } - maskRemaining.set(mask); - } else { - maskRemaining.set(""); + + if (getCountryCallingCode() == null && !isForceLocalNumber()) { + resolveCountryCode(change); + } else { + if (change.isAdded()) { + numbersAdded(change); + } else if (change.isReplaced()) { + // Not allowed + change = null; + } else if (change.isDeleted()) { + numbersRemoved(change); + } + } + + } finally { + selfUpdate = false; } + + return change; } - private boolean isUnMasked() { - return getMask() == null || getMask().isEmpty(); + private void resolveCountryCode(TextFormatter.Change change) { + String newText = change.getControlNewText(); + PhoneNumber number = parser.call(newText); + if (number != null) { + // TODO this won't ever pick a sub country, since always top countries are resolved first. Try to + // find a solution for this + setPhoneNumber(String.valueOf(number.countryCallingCode.phonePrefix())); + PhoneNumberField.this.setFormattedLocalPhoneNumber(number.localPhoneNumber); + updateMaskRemaining(number.localPhoneNumber); + change.setText(""); + change.setCaretPosition(0); + change.setAnchor(0); + change.setRange(0, 0); + } } - private TextFormatter.Change numbersAdded(TextFormatter.Change change) { + private void numbersAdded(TextFormatter.Change change) { if (isUnMasked()) { - return change; + setPhoneNumber(undoFormat(change.getControlNewText())); + return; } String remainingMask = getMaskRemaining(); @@ -906,6 +944,9 @@ private TextFormatter.Change numbersAdded(TextFormatter.Change change) { for (char number : originalText.toCharArray()) { if (remainingMask.isEmpty()) { + if (isStrictMode()) { + break; + } newText.append(number); continue; } @@ -929,13 +970,12 @@ private TextFormatter.Change numbersAdded(TextFormatter.Change change) { setMaskRemaining(remainingMask); setPhoneNumber(undoFormat(change.getControlNewText())); - - return change; } - private TextFormatter.Change numbersRemoved(TextFormatter.Change change) { + private void numbersRemoved(TextFormatter.Change change) { if (isUnMasked()) { - return change; + setPhoneNumber(undoFormat(change.getControlNewText())); + return; } String originalText = change.getControlNewText(); @@ -955,8 +995,6 @@ private TextFormatter.Change numbersRemoved(TextFormatter.Change change) { String formattedPhone = change.getControlNewText(); setPhoneNumber(undoFormat(formattedPhone)); updateMaskRemaining(formattedPhone); - - return change; } private String undoFormat(String formattedPhoneNumber) {