angulardart / angular

Fast and productive web framework provided by Dart
https://pub.dev/packages/angular
MIT License
1.83k stars 231 forks source link

preventDefault stops ngModel input change detection #1846

Open insinfo opened 5 years ago

insinfo commented 5 years ago

I was creating a money mask directive, in my app I came across this problem, because in my mascara implementation I use the "event.preventDefault ();" in the onKeyDown event to prevent nonnumeric characters from entering an input. This disrupts the function of [(ngModel)], ie the value of the input stops being passed to the model.

Hypothetical example for demonstration

  <input moneyMask [(ngModel)]="item.preco" [maxlength]="11" type="text" class="form-control">

@Directive(selector: '[moneyMask]')
class MoneyMaskDirective {
  InputElement inputElement;
  //var options = ;
  String str = "";

  MoneyMaskDirective(Element element) {
    inputElement = element as InputElement;
    inputElement.onKeyDown.listen(onKeyDown);   
  }

onKeyDown(e) {
    KeyboardEvent event = e as KeyboardEvent;
    int keyCode = event.keyCode;
    event.preventDefault();  
    inputElement.value = String.fromCharCodes([keyCode]);
......................
}
insinfo commented 5 years ago

my mask implementation is working correctly but the angular does not detect the input change

import 'dart:html';
import 'dart:async';
import 'dart:math';

import 'package:angular/angular.dart';

//  <!-- <input maskMoney  [maxlength]="11"  type="text" > -->
@Directive(selector: '[maskMoney]')
class MaskMoneyDirective {
  /* @Input()
  Map<String, String> maskMoney;*/
  InputElement inputElement;
  MaskMoneyOptions options = new MaskMoneyOptions();
  StreamSubscription strSubKeyUp;
  StreamSubscription strSubKeyDown;
  final Element _el;
  var onFocusValue;

  var browser = {};

  MaskMoneyDirective(this._el) {
    browser['mozilla'] = RegExp('mozilla').hasMatch(window.navigator.userAgent.toLowerCase()) &&
        !RegExp('webkit').hasMatch(window.navigator.userAgent.toLowerCase());
    browser['webkit'] = RegExp('webkit').hasMatch(window.navigator.userAgent.toLowerCase());
    browser['opera'] = RegExp('opera').hasMatch(window.navigator.userAgent.toLowerCase());
    browser['msie'] = RegExp('msie').hasMatch(window.navigator.userAgent.toLowerCase());
    browser['device'] = RegExp('android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini', caseSensitive: false)
        .hasMatch(window.navigator.userAgent.toLowerCase());

    inputElement = _el;

    inputElement.onKeyPress.listen(keypressEvent);
    inputElement.onKeyDown.listen(keydownEvent);
    inputElement.onClick.listen(clickEvent);
    inputElement.onBlur.listen(blurEvent);
    inputElement.onFocus.listen(focusEvent);
    inputElement.onDoubleClick.listen(doubleClickEvent);
    inputElement.onCut.listen(cutPasteEvent);
    inputElement.onPaste.listen(cutPasteEvent);
  }

  unmasked() {
    var value = inputElement.value != "" ? inputElement.value : "0";
    var isNegative = value.indexOf("-") != -1;
    String decimalPart;
    // get the last position of the array that is a number(coercion makes "" to be evaluated as false)
    var reversed = value.split(r'\D').reversed.toList();
    for (var element in reversed) {
      if (element != null) {
        decimalPart = element;
        break;
      }
    }

    value = value.replaceAll(new RegExp('\D'), "");
    value = value.replaceAll(new RegExp(decimalPart + "\$"), "." + decimalPart);
    if (isNegative) {
      value = "-" + value;
    }
    return double.tryParse(value);
  }

  Selection getInputSelection() {
    var el = inputElement, start = 0, end = 0;
    start = el.selectionStart;
    end = el.selectionEnd;
    return new Selection(start: start, end: end);
  } //getInputSelection

  //pode introduzir mais números
  canInputMoreNumbers() {
    //Comprimento Máximo Atingido
    var haventReachedMaxLength = false;
    var maxlength = inputElement.getAttribute("maxlength");
    if (maxlength != null) {
      int maxl = int.tryParse(maxlength);
      if (maxl != null && maxl >= 0) {
        haventReachedMaxLength = true;
      }
    }

    var selection = getInputSelection();
    var start = selection.start;
    var end = selection.end;
    //tem número selecionado
    var haveNumberSelected = false;
    if (selection.start != selection.end) {
      var re = inputElement.value.substring(start, end);
      if (re != null) {
        if (re.allMatches(r'\d').length > 0) {
          haveNumberSelected = true;
        }
      }
    }

    var startWithZero = false;
    if (inputElement.value.length > 0) {
      startWithZero = inputElement.value.startsWith("0");
    }
    var result = haventReachedMaxLength || haveNumberSelected || startWithZero;
    print(result);
    return result;
  }

  setCursorPosition(pos) {
    // Do not set the position if
    // the we're formatting on blur.
    // This is because we do not want
    // to refocus on the control after
    // the blur.
    if (!!options.formatOnBlur) {
      return;
    }
    inputElement.focus();
    inputElement.setSelectionRange(pos, pos);
  }

  maskAndPosition(startPos) {
    var originalLen = inputElement.value.length, newLen;
    inputElement.value = maskValue(inputElement.value);
    newLen = inputElement.value.length;
    // If the we're using the reverse option,
    // do not put the cursor at the end of
    // the input. The reverse option allows
    // the user to input text from left to right.
    if (!options.reverse) {
      startPos = startPos - (originalLen - newLen);
    }
    setCursorPosition(startPos);
  }

  mask() {
    String value = inputElement.value;
    if (options.allowEmpty && value == "") {
      return;
    }
    print("mask value " + value);
    var isNumber = !isNotNumeric(value);
    print("mask isNumber " + isNumber.toString());
    var decimalPointIndex = isNumber ? value.indexOf(".") : value.indexOf(options.decimal);
    if (options.precision > 0) {
      if (decimalPointIndex < 0) {
        print("decimalPointIndex $decimalPointIndex");
        value += options.decimal + List<String>.generate(options.precision, (i) => "0").join();
      } else {
        // If the following decimal part dosen't have enough length
        //against the precision, it needs to be filled with zeros.
        //Se a parte decimal a seguir não tiver comprimento suficiente
        //em relação à precisão, ela precisará ser preenchida com zeros.

        var integerPart = value.substring(0, decimalPointIndex);
        var decimalPart = value.substring(decimalPointIndex + 1);

        String numberZerosRequired = (options.precision - decimalPart.length) < 1
            ? ""
            : List<String>.generate(options.precision - decimalPart.length, (i) => "0").join();

        value = integerPart + options.decimal + decimalPart + numberZerosRequired;
      }
    } else if (decimalPointIndex > 0) {
      // if the precision is 0, discard the decimal part
      value = value.substring(0, decimalPointIndex);
    }
    print("mask value " + value);
    var result = maskValue(value);
    print("mask result " + result);
    inputElement.value = result;
  }

  changeSign() {
    var inputValue = inputElement.value;
    if (options.allowNegative) {
      if (inputValue != "" && inputValue.substring(0, 1) == "-") {
        return inputValue.replaceAll("-", "");
      } else {
        return "-" + inputValue;
      }
    } else {
      return inputValue;
    }
  }

  preventDefault(e) {
    if (e.preventDefault != null) {
      //standard browsers
      e.preventDefault();
    } else {
      // old internet explorer
      e.returnValue = false;
    }
  }

  keypressEvent(KeyboardEvent e) {
    var key = e.keyCode;
    var decimalKeyCode = options.decimal.codeUnitAt(0);
    //added to handle an IE "special" event
    if (key == null) {
      return false;
    }

    // any key except the numbers 0-9. if we're using settings.reverse,
    // allow the user to input the decimal key
    //Qualquer tecla, exceto os números 0-9. se estivermos usando settings.reverse,
    //permitir que o usuário insira a tecla decimal
    if ((key < 48 || key > 57) && (key != decimalKeyCode || !options.reverse)) {
      print("keypressEvent !options.reverse");
      return handleAllKeysExceptNumericalDigits(key, e);
    } else if (!canInputMoreNumbers()) {
      print("keypressEvent !canInputMoreNumbers()");
      return false;
    } else {
      if (key == decimalKeyCode && shouldPreventDecimalKey()) {
        return false;
      }
      if (options.formatOnBlur) {
        return true;
      }

      preventDefault(e);
      print("keypressEvent else");
      applyMask(e);
      return false;
    }
  }

  shouldPreventDecimalKey() {
    // If all text is selected, we can accept the decimal
    // key because it will replace everything.
    if (isAllTextSelected()) {
      return false;
    }

    return alreadyContainsDecimal();
  }

  isAllTextSelected() {
    var length = inputElement.value.length;
    var selection = getInputSelection();
    // This should if all text is selected or if the
    // input is empty.
    return selection.start == 0 && selection.end == length;
  }

  applyMask(e) {
    var key = e.keyCode, keyPressedChar = "", selection, startPos, endPos, value;
    if (key >= 48 && key <= 57) {
      keyPressedChar = String.fromCharCode(key);
    }
    selection = getInputSelection();
    startPos = selection.start;
    endPos = selection.end;
    value = inputElement.value;
    print("applyMask $value");
    var result = value.substring(0, startPos) + keyPressedChar + value.substring(endPos, value.length);
    inputElement.value = result;
    print("applyMask result");
    maskAndPosition(startPos + 1);
  }

  handleAllKeysExceptNumericalDigits(key, e) {
    // -(minus) key
    if (key == 45) {
      inputElement.value = changeSign();
      return false;
      // +(plus) key
    } else if (key == 43) {
      inputElement.value = inputElement.value.replaceAll("-", "");
      return false;
      // enter key or tab key
    } else if (key == 13 || key == 9) {
      return true;
    } else if (browser['mozilla'] && (key == 37 || key == 39) && e.charCode == 0) {
      // needed for left arrow key or right arrow key with firefox
      // the charCode part is to avoid allowing "%"(e.charCode 0, e.keyCode 37)
      return true;
    } else {
      // any other key with keycode less than 48 and greater than 57
      preventDefault(e);
      return true;
    }
  }

  alreadyContainsDecimal() {
    return inputElement.value.indexOf(options.decimal) > -1;
  }

  keydownEvent(e) {
    var key = e.keyCode, selection, startPos, endPos, value, lastNumber;
    //needed to handle an IE "special" event
    if (key == null) {
      return false;
    }

    print("keydownEvent key $key");

    selection = getInputSelection();
    startPos = selection.start;
    endPos = selection.end;

    print("keydownEvent startPos $startPos");
    print("keydownEvent endPos $endPos");

    if (key == 8 || key == 46 || key == 63272) {
      // backspace or delete key (with special case for safari)
      preventDefault(e);

      value = inputElement.value;

      // not a selection
      if (startPos == endPos) {
        // backspace
        if (key == 8) {
          if (options.suffix == "") {
            startPos -= 1;
          } else {
            // needed to find the position of the last number to be erased
            lastNumber = value.split("").reverse().join("").search(r'\d');
            startPos = value.length - lastNumber - 1;
            endPos = startPos + 1;
          }
          //delete
        } else {
          endPos += 1;
        }
      }

      inputElement.value = (value.substring(0, startPos) + value.substring(endPos, value.length));

      maskAndPosition(startPos);
      return false;
    } else if (key == 9) {
      // tab key
      return true;
    } else {
      // any other key
      return true;
    }
  }

  focusEvent(e) {
    onFocusValue = inputElement.value;
    mask();
    var input = inputElement, textRange;

    if (!!options.selectAllOnFocus) {
      input.select();
    }
  }

  cutPasteEvent(e) {
    var future = new Future.delayed(const Duration(milliseconds: 0), () {
      mask();
    });
  }

  getDefaultMask() {
    double n = double.tryParse("0") / pow(10, options.precision);
    return (n.toStringAsFixed(options.precision)).replaceAll(new RegExp("\\."), options.decimal);
  }

  blurEvent(e) {
    if (browser['msie']) {
      keypressEvent(e);
    }

    if (!!options.formatOnBlur && inputElement.value != onFocusValue) {
      applyMask(e);
    }

    if (inputElement.value == "" && options.allowEmpty) {
      inputElement.value = "";
    } else if (inputElement.value == "" || inputElement.value == setSymbol(getDefaultMask())) {
      if (!options.allowZero) {
        inputElement.value = "";
      } else if (!options.affixesStay) {
        inputElement.value = getDefaultMask();
      } else {
        inputElement.value = setSymbol(getDefaultMask());
      }
    } else {
      if (!options.affixesStay) {
        var newValue = inputElement.value.replaceAll(options.prefix, "").replaceAll(options.suffix, "");
        inputElement.value = newValue;
      }
    }
    if (inputElement.value != onFocusValue) {
      inputElement.onChange.listen((e) {});
    }
  }

  clickEvent(e) {
    var input = inputElement, length;
    if (!!options.selectAllOnFocus) {
      // selectAllOnFocus will be handled by
      // the focus event. The focus event is
      // also fired when the input is clicked.
      return;
    } else if (input.setSelectionRange != null && options.bringCaretAtEndOnFocus) {
      length = inputElement.value.length;
      input.setSelectionRange(length, length);
    } else {
      inputElement.value = inputElement.value;
    }
  }

  doubleClickEvent(e) {
    var input = inputElement, start, length;
    if (input.setSelectionRange != null && options.bringCaretAtEndOnFocus) {
      length = inputElement.value.length;
      start = options.doubleClickSelection ? 0 : length;
      input.setSelectionRange(start, length);
    } else {
      inputElement.value = inputElement.value;
    }
  }

  setSymbol(String value) {
    var oper = "";
    if (value.indexOf("-") > -1) {
      value = value.replaceAll("-", "");
      oper = "-";
    }
    if (value.indexOf(options.prefix) > -1) {
      value = value.replaceAll(options.prefix, "");
    }
    if (value.indexOf(options.suffix) > -1) {
      value = value.replaceAll(options.suffix, "");
    }
    var r = oper + options.prefix + value + options.suffix;
    print("setSymbol $r");
    return r;
  }

  maskValue(String value) {
    if (options.allowEmpty && value == "") {
      return "";
    }
    if (!!options.reverse) {
      var r = maskValueReverse(value);
      print("maskValueReverse $r");
      return r;
    }
    var result = maskValueStandard(value);
    print("maskValueStandard $result");
    return result;
  }

  maskValueStandard(String value) {
    String negative = (value.indexOf("-") > -1 && options.allowNegative) ? "-" : "";
    print("negative " + negative);
    String onlyNumbers = value.replaceAll(RegExp('[^0-9]'), "");
    print("onlyNumbers " + onlyNumbers);
    String integerPart = "0";
    if (onlyNumbers.length > 1) {
      integerPart = onlyNumbers.substring(0, onlyNumbers.length - options.precision);
    }
    print("integerPart " + integerPart);
    String newValue;
    String decimalPart;
    String leadingZeros;

    newValue = buildIntegerPart(integerPart, negative);

    if (options.precision > 0) {
      if (!isNotNumeric(value) && value.indexOf(".") > -1) {
        var precision = value.substring(value.indexOf(".") + 1);
        onlyNumbers += new List<String>.generate((options.precision) - precision.length, (e) => "0").join();
        integerPart = onlyNumbers.substring(0, onlyNumbers.length - options.precision);
        newValue = buildIntegerPart(integerPart, negative);
      }
      if (onlyNumbers.length > options.precision) {
        decimalPart = onlyNumbers.substring(onlyNumbers.length - options.precision);
      }else{
        decimalPart = onlyNumbers;
      }
      leadingZeros = new List<String>.generate((options.precision) - decimalPart.length, (e) => "0").join();
      newValue += options.decimal + leadingZeros + decimalPart;
    }

    print("newValue " + newValue);
    print("decimalPart " + decimalPart);
    print("leadingZeros " + leadingZeros);
    return setSymbol(newValue);
  }

  maskValueReverse(String value) {
    var negative = value.indexOf("-") > -1 && options.allowNegative ? "-" : "";
    var valueWithoutSymbol = value.replaceAll(options.prefix, "").replaceAll(options.suffix, "");
    var integerPart = valueWithoutSymbol.split(options.decimal)[0];
    var newValue;
    var decimalPart = "";

    if (integerPart == "") {
      integerPart = "0";
    }
    newValue = buildIntegerPart(integerPart, negative);

    if (options.precision > 0) {
      var arr = valueWithoutSymbol.split(options.decimal);
      if (arr.length > 1) {
        decimalPart = arr[1];
      }
      newValue += options.decimal + decimalPart;
      var rounded = double.tryParse((integerPart + "." + decimalPart)).toStringAsFixed(options.precision);
      var roundedDecimalPart = rounded.toString().split(options.decimal)[1];
      newValue = newValue.split(options.decimal)[0] + "." + roundedDecimalPart;
    }

    return setSymbol(newValue);
  }

  buildIntegerPart(integerPart, negative) {
    // remove initial zeros
    integerPart = integerPart.replaceAll(RegExp('^0*'), "");

    // put settings.thousands every 3 chars
    integerPart = integerPart.replaceAll(RegExp('\B(?=(\d{3})+(?!\d))'), options.thousands);
    if (integerPart == "") {
      integerPart = "0";
    }
    return negative + integerPart;
  }

  bool isNumeric(String s) {
    if (s == null) {
      return false;
    }
    return double.parse(s, (e) => null) != null;
  }

  bool isNotNumeric(String s) {
    if (s == null) {
      return true;
    } else if (s.trim() == "") {
      return false;
    }
    return double.parse(s, (e) => null) == null;
  }

  //print(dynamic item) {}
}

class Selection {
  int start, end;
  Selection({this.start, this.end});
}

class MaskMoneyOptions {
  String prefix = "R\$ ";
  String suffix = "";
  bool affixesStay = false;
  String thousands = ".";
  String decimal = ",";
  int precision = 2;
  bool allowZero = false;
  bool allowNegative = true;
  bool doubleClickSelection = true;
  bool allowEmpty = false;
  bool bringCaretAtEndOnFocus = true;
  bool formatOnBlur = false;
  bool reverse = false;
  bool selectAllOnFocus = false;
}