Javascript: Set cursor position when changing the value of input - javascript

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?

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)
});

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.

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

Related

Is there a way to speed up a function's completion in Javascript?

I am very new to Javascript as I began self-learning just yesterday. I am trying to emulate a type racer game.
(spacebar) as the input will signify the completion of my attempt to type the current word, and I have functions to check if it is correct/incorrect, and then resets the input.
So if I type apple and then (space), the input field becomes reset.
However the problem is if the user types too quickly, then the first or more character of the next word is included in the input before the reset function.
Example:
words to type: apple lemon
user input: apple lemon
input field: apple le -> upon space, reset() is called and clears input but user already typed le once reset() is completed -> mon
So I am wondering if there is a way to speed up reset() such that it is called and completed before the next input from the user.
I initially had everything under 1 function "inputMatch", that was called upon keyup, but tried to separate the reset() and inputMatch() functions by having one being called upon keydown, and the other upon keyup.
This is the code I have. I can provide more context/the full code if needed, but since I am new here, I am unsure whether people prefer to read simplified pseudo code or actual code.
<input type="text" class="form-control" id="inputfield" value="" dir="ltr" placeholder="" onkeyup="reset()" onkeydown="inputMatch(event)">
<script>
const inputMatch = () => {
var current = word that I am currently attempting to type;
if(event.keyCode == 32){ //spacebar
change word that was typed to be either correct or incorrect
};
//once at end need to remove the first (current.wordnr - 1) words
}
const reset = () => {
if(event.keyCode == 32){ //spacebar
document.getElementById('inputfield').value = ""; //clear input field
}
}
</script>
Welcome to Stack Overflow! My recommendation would be to use the oninput event handler instead of onkeyup/onkeydown. See the following code:
const handle = (input) => {
if (input.charAt(input.length - 1) === ' ') // If the last character is a space
reset()
else // otherwise, handle your input (in your case, use it in your type racing game)
console.log("Partial word: " + input)
}
const inputField = document.getElementById("inputfield")
const reset = () => {
console.log("Entered word: " + inputField.value.trimRight())
inputField.value = ""
}
<input type="text" id="inputfield" value="" dir="ltr" placeholder="" oninput="handle(this.value)">
oninput runs synchronously, meaning that it's impossible for the user to type too quickly. It also makes the code a lot simpler.

Regex for negative money format - 2decimal place & can go negative

I have an input and on input change, I change it to the money format.
<input type="number" class="currency" step="0.01">
like this:
$('input[type="number"].currency').on('input', function(e) {
this.value = this.value.match(/^-?\d+\.?\d{0,2}/);
});
However, as this gets triggered on every keypress, it doesn't let me write "-" as first character; so it erases it as soon as I click "-"
What kind of regex can I use for allowing that?
https://jsfiddle.net/4zv9wLr3/ - Here is a fiddle, try to write a negative number, it won't allow you to write.
When "number" input triggers the input event, the value was received blank.
$('input[type="number"].currency').on('input', function(e) {
if (e.target.value.length !== 0) {
this.value = this.value.match(/^-?\d+\.?\d{0,2}/);
}
});
I added a guard like this and it worked.

jQuery: focusout triggering before onclick for Ajax suggestion

I have a webpage I'm building where I need to be able to select 1-9 members via a dropdown, which then provides that many input fields to enter their name. Each name field has a "suggestion" div below it where an ajax-fed member list is populated. Each item in that list has an "onclick='setMember(a, b, c)'" field associated with it. Once the input field loses focus we then validate (using ajax) that the input username returns exactly 1 database entry and set the field to that entry's text and an associated hidden memberId field to that one entry's id.
The problem is: when I click on the member name in the suggestion box the lose focus triggers and it attempts to validate a name which has multiple matches, thereby clearing it out. I do want it to clear on invalid, but I don't want it to clear before the onclick of the suggestion box name.
Example:
In the example above Paul Smith would populate fine if there was only one name in the suggestion list when it lost focus, but if I tried clicking on Raphael's name in the suggestion area (that is: clicking the grey div) it would wipe out the input field first.
Here is the javascript, trimmed for brevity:
function memberList() {
var count = document.getElementById('numMembers').value;
var current = document.getElementById('listMembers').childNodes.length;
if(count >= current) {
for(var i=current; i<=count; i++) {
var memberForm = document.createElement('div');
memberForm.setAttribute('id', 'member'+i);
var memberInput = document.createElement('input');
memberInput.setAttribute('name', 'memberName'+i);
memberInput.setAttribute('id', 'memberName'+i);
memberInput.setAttribute('type', 'text');
memberInput.setAttribute('class', 'ajax-member-load');
memberInput.setAttribute('value', '');
memberForm.appendChild(memberInput);
// two other fields (the ones next to the member name) removed for brevity
document.getElementById('listMembers').appendChild(memberForm);
}
}
else if(count < current) {
for(var i=(current-1); i>count; i--) {
document.getElementById('listMembers').removeChild(document.getElementById('listMembers').lastChild);
}
}
jQuery('.ajax-member-load').each(function() {
var num = this.id.replace( /^\D+/g, '');
// Update suggestion list on key release
jQuery(this).keyup(function(event) {
update(num);
});
// Check for only one suggestion and either populate it or clear it
jQuery(this).focusout(function(event) {
var number = this.id.replace( /^\D+/g, '');
memberCheck(number);
jQuery('#member'+number+'suggestions').html("");
});
});
}
// Looks up suggestions according to the partially input member name
function update(memberNumber) {
// AJAX code here, removed for brevity
self.xmlHttpReq.onreadystatechange = function() {
if (self.xmlHttpReq.readyState == 4) {
document.getElementById('member'+memberNumber+'suggestions').innerHTML = self.xmlHttpReq.responseText;
}
}
}
// Looks up the member by name, via ajax
// if exactly 1 match, it fills in the name and id
// otherwise the name comes back blank and the id is 0
function memberCheck(number) {
// AJAX code here, removed for brevity
if (self.xmlHttpReq.readyState == 4) {
var jsonResponse = JSON.parse(self.xmlHttpReq.responseText);
jQuery("#member"+number+"id").val(jsonResponse.id);
jQuery('#memberName'+number).val(jsonResponse.name);
}
}
}
function setMember(memberId, name, listNumber) {
jQuery("#memberName"+listNumber).val(name);
jQuery("#member"+listNumber+"id").val(memberId);
jQuery("#member"+listNumber+"suggestions").html("");
}
// Generate members form
memberList();
The suggestion divs (which are now being deleted before their onclicks and trigger) simply look like this:
<div onclick='setMember(123, "Raphael Jordan", 2)'>Raphael Jordan</div>
<div onclick='setMember(450, "Chris Raptson", 2)'>Chris Raptson</div>
Does anyone have any clue how I can solve this priority problem? I'm sure I can't be the first one with this issue, but I can't figure out what to search for to find similar questions.
Thank you!
If you use mousedown instead of click on the suggestions binding, it will occur before the blur of the input. JSFiddle.
<input type="text" />
Click
$('input').on('blur', function(e) {
console.log(e);
});
$('a').on('mousedown', function(e) {
console.log(e);
});
Or more specifically to your case:
<div onmousedown='setMember(123, "Raphael Jordan", 2)'>Raphael Jordan</div>
using onmousedown instead of onclick will call focusout event but in onmousedown event handler you can use event.preventDefault() to avoid loosing focus. This will be useful for password fields where you dont want to loose focus on input field on click of Eye icon to show/hide password

Help me improve this Javascript codes limitations?

This Javascript which uses functions from jQuery is quite handy but getting feedback on it there is some limitations which I was hoping you guys could help me overcome.
The function basically creates a textbox with a formatted time (HH:MM:SS) so that it is easy for users to enter in times rather than have to use a date time picker which involves lots of clicks.
Code:
//global variable for keeping count on how many times the function is called
var i = 0;
//for adding formatted time fields
function timemaker()
{
//creates an input field of text type formatted with a value of "00:00:00"
var input = $("<input>",
{
name: 'time'+i, // e.g. time0, time1, time2, time3
value: '00:00:00',
maxlength: '8',
size: '6'
});
//this function which uses jquery plugin causes the cursor in the field to goto position zero
//when selected making it easier for the user to enter times and not need to select the correct position
input.click(function()
{
$(this).prop({
selectionStart: 0,
selectionEnd: 0
});
//this child function moves the cursor along the text field
//when it reaches the first ":" it jumps to the next "00"
}).keydown(function() {
if (event.keyCode == 9)
{
return;
}
else
{
var sel = $(this).prop('selectionStart'),
val = $(this).val(),
newsel = sel === 2 ? 3: sel;
newsel = sel === 5 ? 6: newsel;
$(this).val(val.substring(0, newsel) + val.substring(newsel + 1))
.prop({
selectionStart: newsel,
selectionEnd: newsel
});
}
});
//this appends the text field to a divved area of the page
input.appendTo("#events");
i++;
return;
}
00:00:00
Limitations I need help with
Say for example you wanted to enter a time of 12:45:00 , you
obviously don't need to enter the seconds part (last "00") as they
are already there. So you then decide to tab out of that text field
but the javascript interprets your "Tab" keypress as an entry and
deletes a zero from the field causing the value to be like 12:45:0
Does not validate inputs for 24 hour format- do you think it will be
possible to do that? e.g. first number you enter is "2" therefore the
only options you have are "0,1,2,3"
If you make a mistake in the 4th digit and reselect the text field
you have to enter everything again.
I think the main thing you're missing that would allow you to implement all those requirements is the argument that jQuery passes to you in your keydown event handler. So, change that line to this:
.keydown(function(event){
if (event.keyCode == 9) { return; }
... the rest of your code ...
and then you can use event.keyCode to identify what was pressed and take actions accordingly. So for example, if event.keyCode == 9 then the user pressed TAB.
This is a slightly out-of-the-box solution, but you might consider it if things don't work out with your filtered textbox:
http://jsfiddle.net/YLcYS/4/

BB Code Parser (in formatting phase) with jQuery jammed due to messed up loops most likely

Greetings everyone,
I'm making a BB Code Parser but I'm stuck on the JavaScript front. I'm using jQuery and the caret library for noting selections in a text field. When someone selects a piece of text a div with formatting options will appear.
I have two issues.
Issue 1. How can I make this work for multiple textfields? I'm drawing a blank as it currently will detect the textfield correctly until it enters the
$("#BBtoolBox a").mousedown(function() { }
loop. After entering it will start listing one field after another in a random pattern in my eyes.
!!! MAIN Issue 2. I'm guessing this is the main reason for issue 1 as well. When I press a formatting option it will work on the first action but not the ones afterwards. It keeps duplicating the variable parsed. (if I only keep to one field it will never print in the second)
Issue 3 If you find anything especially ugly in the code, please tell me how to improve myself.
I appriciate all help I can get. Thanks in advance
$(document).ready(function() {
BBCP();
});
function BBCP(el) {
if(!el) { el = "textarea"; }
// Stores the cursor position of selection start
$(el).mousedown(function(e) {
coordX = e.pageX;
coordY = e.pageY;
// Event of selection finish by using keyboard
}).keyup(function() {
BBtoolBox(this, coordX, coordY);
// Event of selection finish by using mouse
}).mouseup(function() {
BBtoolBox(this, coordX, coordY);
// Event of field unfocus
}).blur(function() {
$("#BBtoolBox").hide();
});
}
function BBtoolBox(el, coordX, coordY) {
// Variable containing the selected text by Caret
selection = $(el).caret().text;
// Ignore the request if no text is selected
if(selection.length == 0) {
$("#BBtoolBox").hide();
return;
}
// Print the toolbox
if(!document.getElementById("BBtoolBox")) {
$(el).before("<div id=\"BBtoolBox\" style=\"left: "+ ( coordX + 5 ) +"px; top: "+ ( coordY - 30 ) +"px;\"></div>");
// List of actions
$("#BBtoolBox").append("<img src=\"./icons/text_bold.png\" alt=\"B\" title=\"Bold\" />");
$("#BBtoolBox").append("<img src=\"./icons/text_italic.png\" alt=\"I\" title=\"Italic\" />");
} else {
$("#BBtoolBox").css({'left': (coordX + 3) +'px', 'top': (coordY - 30) +'px'}).show();
}
// Parse the text according to the action requsted
$("#BBtoolBox a").mousedown(function() {
switch($(this).children(":first").attr("alt"))
{
case "B": // bold
parsed = "[b]"+ selection +"[/b]";
break;
case "I": // italic
parsed = "[i]"+ selection +"[/i]";
break;
}
// Changes the field value by replacing the selection with the variable parsed
$(el).val($(el).caret().replace(parsed));
$("#BBtoolBox").hide();
return false;
});
}
This line: $("#BBtoolBox a").mousedown(function() attaches a function to the links. However, this line is run multiple times, and each time it runs it attaches another function to the links, leaving you with duplicated text.
An optimal solution is to use a plugin, for example (the first one I found): http://urlvars.com/code/example/19/using-jquery-bbcode-editor (demo)

Categories

Resources