set caret back to it's original place after fo

2019-07-25 10:46发布

问题:

I have an input field which has a data manipulation and is a decimal field. Everything works fine except when I put in more than 3 numbers it will lose the current caret and set it to the end of the field because of the field formatting. Example: 123 works fine 1234 will result in 1’234.00 and the caret is after the last 0. How is it possible to set the caret back to its original position? (Between 4 and the .)

function thousenderSign(number) {
  number = '' + number;
  if (number.length > 3) {
    var mod = number.length % 3;
    var output = (mod > 0 ? (number.substring(0, mod)) : '');
    for (i = 0; i < Math.floor(number.length / 3); i++) {
      if ((mod == 0) && (i == 0)) {
        output += number.substring(mod + 3 * i, mod + 3 * i + 3);
      } else {
        output += "'" + number.substring(mod + 3 * i, mod + 3 * i + 3); // set the sign
      }
    }
    return (output);
  } else return number;
}
ko.extenders.numeric = function(target, precision) {
  var result = ko.pureComputed({
    read: target,
    write: function(newValue) {
      var current = target();
      var roundingMultiplier = Math.pow(10, precision);
      var newValueAsNum = null;

      if (newValue !== undefined && newValue !== 0 && newValue !== null) {
        newValueAsNum = newValue.toString().replace("'", "");
        // provide only int fort he function
        var onlyInt = newValueAsNum.split(".");
        // Remove more then 2 digits after the dot
        if (onlyInt.length > 1 && onlyInt[1].length > 2) {
          onlyInt[1] = onlyInt[1].toString().substring(0, 2);
        }
      }

      var valueToWrite = (Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier) === 0 ? null : Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;

      // thousender sign
      if (newValueAsNum !== null && newValueAsNum.length > 3) {
        valueToWrite = thousenderSign(onlyInt[0]) + "." + (onlyInt.length > 1 ? onlyInt[1] : '00');
      }
      if (valueToWrite !== current) {
        target(valueToWrite);
      } else {
        if (newValue !== current) {
          target.notifySubscribers(valueToWrite);
        }
      }
    }
  }).extend({
    notify: 'always'
  });
  result(target());
  return result;
};


function ExampleViewModel() {
  self = this;
  self.counterofferPremium = ko.observable().extend({
    numeric: 2
  });
};

var viewModel = new ExampleViewModel();
ko.applyBindings(viewModel);
<!doctype html>
<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
</head>

<body>
  <input data-bind="value: counterofferPremium, valueUpdate: 'afterkeydown'" type="text" />
</body>

</html>

回答1:

The simplest thing to do is get rid of the valueUpdate setting so that the formatting happens after the user is done editing the number.

To have it work interactively, you need to add an input event handler that

  1. Finds how many digits are behind the cursor (this happens before reformatting)
  2. Does a setTimeout to allow the reformatting to happen
  3. Sets the cursor position after the same number of digits

Also note that your formatter gets weird for very long numbers. You might want to replace it with a call to toLocaleString with some additional substitutions.

function thousenderSign(number) {
  number = '' + number;
  if (number.length > 3) {
    var mod = number.length % 3;
    var output = (mod > 0 ? (number.substring(0, mod)) : '');
    for (i = 0; i < Math.floor(number.length / 3); i++) {
      if ((mod == 0) && (i == 0)) {
        output += number.substring(mod + 3 * i, mod + 3 * i + 3);
      } else {
        output += "'" + number.substring(mod + 3 * i, mod + 3 * i + 3); // set the sign
      }
    }
    return (output);
  } else return number;
}
ko.extenders.numeric = function(target, precision) {
  var result = ko.pureComputed({
    read: target,
    write: function(newValue) {
      var current = target();
      var roundingMultiplier = Math.pow(10, precision);
      var newValueAsNum = null;

      if (newValue !== undefined && newValue !== 0 && newValue !== null) {
        newValueAsNum = newValue.toString().replace("'", "");
        // provide only int fort he function
        var onlyInt = newValueAsNum.split(".");
        // Remove more then 2 digits after the dot
        if (onlyInt.length > 1 && onlyInt[1].length > 2) {
          onlyInt[1] = onlyInt[1].toString().substring(0, 2);
        }
      }

      var valueToWrite = (Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier) === 0 ? null : Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;

      // thousender sign
      if (newValueAsNum !== null && newValueAsNum.length > 3) {
        valueToWrite = thousenderSign(onlyInt[0]) + "." + (onlyInt.length > 1 ? onlyInt[1] : '00');
      }
      if (valueToWrite !== current) {
        target(valueToWrite);
      } else {
        if (newValue !== current) {
          target.notifySubscribers(valueToWrite);
        }
      }
    }
  }).extend({
    notify: 'always'
  });
  result(target());
  return result;
};

function ExampleViewModel() {
  self = this;
  self.counterofferPremium = ko.observable().extend({
    numeric: 2
  });
  self.findPlace = function (data, event) {
    const pos = event.target.selectionEnd;
    var numbersBeforePos = event.target.value.substr(0, pos).replace(/\D/g, '').length;
    setTimeout(function() {
      const formattedValue = event.target.value;
      const numbersNow = event.target.value.replace(/\D/g, '').length;
      
      if (numbersNow >= numbersBeforePos) {
        // find the numbersBeforePos-th number
        const re = /\d/g;
        var newPos;
        while (numbersBeforePos--) {
          newPos = 1 + re.exec(formattedValue).index;
        }
        event.target.setSelectionRange(newPos, newPos);
      }
    }, 0);
  };
};

var viewModel = new ExampleViewModel();
ko.applyBindings(viewModel);
  <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
  <input data-bind="value: counterofferPremium, valueUpdate: 'afterkeydown', event: {input: findPlace}" type="text" />