4

The goal is to have user input converted to upper case as they type. I am using the following extender:

ko.extenders.uppercase = function (target, option) {
    target.subscribe(function (newValue) {
        target(newValue.toUpperCase());
    });
    return target;
};

This extender can be added to the observable that are bound to HTML text boxes:

self.userField = ko.observable(product.userField || "").extend({ uppercase: true });

This works great. Converts text bound to the observable to upper case. As the user types text into an empty field or adds it to the end of existing text, the text is converted to upper case.

So far, so good!

Now when the user wants to insert text, the cursor is placed to the insertion point either with the cursor keys or mouse and the new text is entered the character is inserted as upper case but the cursor position now jumps to the end of the text because the underlying observable is updated.

How can the cursor position be retained while still converting the user input to upper case?

Update 1:

I created a custom binding so I could access the element and get and set the cursor position. I tried to tap into the existing value binding goodness. I am using the built in ko.selectExtensions.writeValue function. This may be good enough for my application but may not work in all browsers.

ko.bindingHandlers.upperCase = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        ko.bindingHandlers["value"].init(element, valueAccessor, allBindings);
    },
    update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var caretPosition = getCaretPosition(element);
        var newValue = ko.utils.unwrapObservable(valueAccessor());
        ko.selectExtensions.writeValue(element, newValue.toUpperCase());

    setCaretPosition(element, caretPosition);

        // Code from: http://stackoverflow.com/questions/2897155/get-cursor-position-in-characters-within-a-text-input-field
        function getCaretPosition (ctrl) {
            var CaretPos = 0;

            // IE Support
            if (document.selection) {
                ctrl.focus ();
                var Sel = document.selection.createRange ();
                Sel.moveStart ('character', -ctrl.value.length);
                CaretPos = Sel.text.length;
            }
            // Firefox support
            else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
                CaretPos = ctrl.selectionStart;
            }

            return (CaretPos);
        }

        function setCaretPosition(ctrl, pos) {
            if(ctrl.setSelectionRange) {
                ctrl.focus();
                ctrl.setSelectionRange(pos,pos);
            }
            else if (ctrl.createTextRange) {
                var range = ctrl.createTextRange();
                range.collapse(true);
                range.moveEnd('character', pos);
                range.moveStart('character', pos);
                range.select();
            }
        }
    }
};
DaveB
  • 9,470
  • 4
  • 39
  • 66
  • Would your bindingHandler on its own be all you need? Or do you have to combine it with the original extender? It seems like your binding handler just shows the field in uppercase, while the model value is still as it was typed. When combined, you seem to get the desired behavior, although then there is no need for the handler to convert to uppercase. The handler basically is there to update a field while retaining its cursor position. – comecme Jun 05 '15 at 18:25
  • @comecme - The bindingHandler alone would be fine. – DaveB Jun 10 '15 at 21:56

2 Answers2

3

After testing custom bindings and the jQuery based solution posted by Thanasis Grammatopoulos, I decided to go the CSS route and first created a new class:

.upper-case { text-transform: uppercase; }

I then applied it to the input elements I wanted to be displayed as upper case:

<input type="text" maxlength="16" class="upper-case" data-bind="value: fhaNum, valueUpdate: 'afterkeydown', disable: isReadOnly" />

Since this doesn't save the text as upper case in the observable, I upper cased the value before sending it to the server:

var data = {
    "fhaNum": self.fhaNum().toUpperCase()
};
DaveB
  • 9,470
  • 4
  • 39
  • 66
  • I used this method, too. Seeing you choose this method combined with the amount of code in the extender really makes this a better solution for my project. – Rhyous Jul 17 '15 at 02:04
  • @Rhyous It may be a better solution for one-off instances, but if, for example, you want your entire view model uppercased, especially across multiple view models, you may be better off doing it at the view model level with an extender. – saluce Nov 27 '17 at 15:59
1

Well, i think the best solution is with Vanilla Javascript. Check the value is UpperCase if not, get position, change value, set position. Easy.

var stayUpperCase = function () {
  if(this.value != this.value.toUpperCase()){
    var caretPos = getCaretPosition(this);
    this.value = this.value.toUpperCase();
    setCaretPosition(this, caretPos);
  }
};
document.getElementById('myinput').addEventListener('keyup', stayUpperCase);
document.getElementById('myinput').addEventListener('change', stayUpperCase);

// Code from http://stackoverflow.com/questions/2897155/get-cursor-position-in-characters-within-a-text-input-field
function getCaretPosition (elem) {
  // Initialize
  var caretPos = 0;

  // IE Support
  if (document.selection) {
    // Set focus on the element
    elem.focus ();
    // To get cursor position, get empty selection range
    var sel = document.selection.createRange ();
    // Move selection start to 0 position
    sel.moveStart ('character', -elem.value.length);
    // The caret position is selection length
    caretPos = sel.text.length;
  }

  // Firefox support
  else if (elem.selectionStart || elem.selectionStart == '0'){
    caretPos = elem.selectionStart;
  }

  // Return results
  return (caretPos);
}

// Code from http://stackoverflow.com/questions/512528/set-cursor-position-in-html-textbox
function setCaretPosition(elem, caretPos) {
  if(elem != null) {
    if(elem.createTextRange) {
      var range = elem.createTextRange();
      range.move('character', caretPos);
      range.select();
    }
    else {
      if(elem.selectionStart) {
        elem.focus();
        elem.setSelectionRange(caretPos, caretPos);
      }
      else
        elem.focus();
    }
  }
}
<input type="text" id="myinput"/>

You can attach more keyboard events to make it looks smoother.

GramThanos
  • 3,572
  • 1
  • 22
  • 34
  • I ended up going in another direction but this answer does address the question. Thanks! – DaveB Oct 09 '14 at 18:43