Javascript: Set cursor position when changing the value of input

10,875

Solution 1

Thanks to Roy J's comment, I was able to find the solution.

Add the following in the updateValue function:

this.$nextTick(() => {
  this.setCaretPosition(myInput, 5)
});

Solution 2

I learned about setSelectionRange from this question, and I used it to handle credit card number input:

template:

<input
    ref="input"
    v-model="value"
    @input="handleChange"
>

instance methods:

data() {
    return {
        lastValue: '',
    }
},

methods: {
    setCursorPosition(el, pos) {
        el.focus();
        el.setSelectionRange(pos, pos);
    },
    handleChange() {
        // handle backspace event
        if (this.value.length < this.lastValue.length) {
            this.lastValue = this.value;
            this.$emit('input-changed', this.value);
            return;
        }
        // handle value-edit event
        if (this.$refs.input.selectionStart < this.value.length) {
            const startPos = this.$refs.input.selectionStart;
            this.value = this.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim();
            this.$nextTick(() => this.setCursorPosition(this.$refs.input, startPos));
            this.lastValue = this.value;
            this.$emit('input-changed', this.value);
            return;
        }
        // handle everything else
        this.value = this.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim();
        this.lastValue = this.value;
        this.$emit('input-changed', this.value);
    },
},

The goal with the above code is to add spaces into a credit card input, so 1234123412341234 is automatically reformatted to 1234 1234 1234 1234. A person venturing into this territory will notice that problems arise when editing the input value.

You can see there are three conditions in my sample above. The last one is the default which simply reformats the current value with a 2-step combo: remove all spaces then adds a space every 4th character.

If you comment out the two if blocks, you can watch the problems emerge.

The first if block handles the backspace event. As you can see, every time the input changes, the value is captured as this.lastValue. When you press backspace, the goal of the first condition is to NOT run the regex. In my opinion, this is better UX. If you comment out that condition, you can see.

The second if block handles the editing events. A good way to test it, is to enter a valid CC but omit the 3rd character, so that everything is off by one. Then add the character in. Everything should be good. Likewise if you backspace multiple characters out. The goal of the second condition is to properly manage the cursor position (or caret position if you prefer that nomenclature).

You can safely delete the first condition and all references to lastValue and the code will still work. This is arguably simpler but worse UX.

Solution 3

I found an easy way to fix this issue, Tested in IE and Chrome 100%

Call this function in each key press

function setCaret(eleId)
 {
   var mousePosition = $(elemId)[0].selectionStart;
   var elem = document.getElementById(elemId);
   elem.setSelectionRange(mousePosition + 1, mousePosition + 1);
 }

Pass text box id to this function, the function will find the mouse position and place the caret for each key press

Share:
10,875
Jonathan Parisot
Author by

Jonathan Parisot

Updated on August 29, 2022

Comments

  • Jonathan Parisot
    Jonathan Parisot over 1 year

    I am trying to reproduce in my app a UX similar to Microsoft Excel / Google Sheets when you type of formula and you have an autocomplete dropdown for the different formulas and variables you have at your disposal.

    For that purpose, after having validated the autocomplete, I want to be able to control the position of the cursor.

    For exemple, if I type =sum(variable1, variable2), on autocomplete of variable2, cursor should be before the last parenthesis and not at the very end.

    I understand how to set the position of the cursor with javascript, the problem is since at the same time I modify the value of the input and set the cursor position, the latter doesn't work.

    I reproduced on fiddle the problem with a simpler context: https://jsfiddle.net/joparisot/j8ourfa1/31/

    My html:

    <div id="app">
        <autocomplete v-model="selection"></autocomplete>
    </div>
    
    <template id="autocomplete">
      <div>
        <h2>gernerogrnio</h2>
        <input id="my-input" 
               class="form-control" 
               type="text" 
               :value="value"
               @keydown.enter="updateValue($event.target.value)">
        <p>{{ value }}</p>
      </div>
    </template>
    

    My script:

        Vue.component('autocomplete', {
        template: '#autocomplete', 
      props: {
        value: {
          type: String,
          required: true
        }
      }, 
      methods: {
        updateValue (value) {
            var new_value = ''
          if (value.length < 4) {
            new_value = 'Inferior'
          } else {
            new_value = 'Superior'
          }
    
          this.$emit('input', new_value)
          var myInput = document.getElementById('my-input');
          this.setCaretPosition(myInput, 5)
        }, 
        setCaretPosition(ctrl, pos) {
            ctrl.focus();
            ctrl.setSelectionRange(pos, pos);
        }
      }
    });
    
    new Vue({
        el: '#app', 
      data: {
        selection: 'test'
      }
    });
    

    I don't bother there with the autocomplete, but depending on what you type, when you press enter, the input will be filled with a new value. You can see that if you comment lines 11 to 16 and just set new_value to value, then setting the cursor position will work.

    I can't seem to be able to do both things at the same time. Any thoughts?

  • Richard
    Richard almost 5 years
    What is this please? I don't see $nextTick in my Vue object when debugging in Chrome
  • Richard
    Richard almost 5 years
    I tried this but it doesn't move the cursor in the textarea: _app._data.sourceText = 'new value'; Vue.nextTick(e.currentTarget.setSelectionRange(2, 2));
  • Carl
    Carl over 4 years
    Why do you emit 'input-changed' instead of 'input'?
  • Carl
    Carl over 4 years
    I've tweaked it a little here, seems like you don't need the events: codesandbox.io/s/vue-template-dbqiy?fontsize=14