Home > front end >  How to automatically place the mouse cursor in an input at a precise index outside the callback() fu
How to automatically place the mouse cursor in an input at a precise index outside the callback() fu

Time:07-13

I have an input in which the user enters an expression of this form:

"TERM":"MATCH_TERM"

I would like that when the user enters a quote in the input, a second quote of the same type is added (the user can use single and double quotes : " or ') and that the mouse cursor is placed between the two quotes that have just been created.

My code is in ReactJs.

I managed to automatically add a second quote when the user enters a first one, at the right place in the string. But I can't figure out how to then move my mouse cursor between the two new quotes.

To make my input component do this, I wrote the following code:

(I tried to simplify the code but normally it is reproducible)

import * as React from "react";
import { useEffect, useState, useRef } from "react";

const QuoteInput: React.FC = () => {
   
    const inputRef = useRef<HTMLInputElement | null>(null);
                     
    const [inputChoose, setInputChoose] = useState("");
    const [previousInputChoose, setPreviousInputChoose] = useState("");

    const [testQuoteAddition, setTestQuoteAddition] = useState(false);
    const [enterSingleQuote, setEnterSingleQuote] = useState(false);
    const [enterDoubleQuote, setEnterDoubleQuote] = useState(false);

    const inputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
        setPreviousRequestChoose(requestChoose);
        const enteredRequest = event.target.value;
        setRequestChoose(enteredRequest);

        setTestQuoteAddition(true);
    };

    function addSingleQuote(indexDifference: number) {
        let newString: string = requestChoose.slice(0,indexDifference   1)   "'"   requestChoose.slice(indexDifference   1);
        setRequestChoose(newString);

        if(inputRef !== null && inputRef !== undefined) {
            if (inputRef.current !== null && inputRef.current !== undefined) {
                console.log("3 ");
                if (inputRef.current.setSelectionRange !== undefined) {
                    inputRef.current.setSelectionRange(indexDifference, indexDifference);
                }
            }
        }
    }

    function addDoubleQuote(indexDifference: number) {
        let newString: string = requestChoose.slice(0,indexDifference   1)   '"'   requestChoose.slice(indexDifference   1);
        setRequestChoose(newString);
    }

    useEffect(()=>{
        if(testQuoteAddition === true) {
            for(let i=0; i<requestChoose.length; i  ) {
                if(previousRequestChoose.charAt(i) !== requestChoose.charAt(i))
                {
                    if (requestChoose.charAt(i) === "'") {
                        setEnterSingleQuote(true);
                    } else if (requestChoose.charAt(i) === '"') {
                        setEnterDoubleQuote(true);
                    }
                }
            }
        }
        setTestQuoteAddition(false);
    },[testQuoteAddition])

    useEffect(()=>{
        if(enterSingleQuote === true){
            let indexDifferenceInRequest: number = requestChoose.length   1;
            let findDifference: boolean = false
            for(let i=0; i<requestChoose.length; i  ) {
                if(previousRequestChoose.charAt(i) !== requestChoose.charAt(i) && findDifference === false)
                {
                    indexDifferenceInRequest = i;
                    findDifference = true;
                }
            }
            addSingleQuote(indexDifferenceInRequest);
            setEnterSingleQuote(false);
        } else if (enterDoubleQuote === true){
            let indexDifferenceInRequest: number = requestChoose.length   1;
            let findDifference: boolean = false
            for(let i=0; i<requestChoose.length; i  ) {
                if(previousRequestChoose.charAt(i) !== requestChoose.charAt(i) && findDifference === false)
                {
                    indexDifferenceInRequest = i;
                    findDifference = true;
                }
            }
            addDoubleQuote(indexDifferenceInRequest);
            setEnterDoubleQuote(false);
        }
    },[enterSingleQuote, enterDoubleQuote])

    return(
        <div>
            <input ref={inputRef} type="text" onChange={inputHandler} value={inputChoose} className="text-center" placeholder="enter an input" />
        </div>
    );
}

export default QuoteInput;

This code allows me to add a new pair of quotes but the mouse cursor is then placed at the end of the string.

If I put: inputRef.current?.setSelectionRange(3, 3); in the inputHandler (the callback function of my input element), every time the user writes the cursor is reset to the third position of the string.

But if I put this same line: inputRef.current?.setSelectionRange(indexDifference, indexDifference); in the function which add a quote, as in the code above, nothing happens, but if I put console.log in the loop it displays well so my conditions are well met and the statement should execute.

I don't see what I'm doing wrong at all, if you could point me in the right direction it would help me a lot.

I just noticed another problem with my add quote function while I was writing this question.

When I type at normal speed in the search bar everything works normally. But if I suddenly decide to write super fast, the function of adding a quote when a user has put a first one stops working and doesn't work at all afterwards even if I start writing again at a normal speed.

Once the users knows how to used my tool well, it may happen that they type very fast and so this use case may occur.

When I display my state variables with console.log, it seems that before typing very fast, everything is triggered in the inputHandler. But after typing very fast, only state variables concerning the old string and the new string entered by the user are activated, the boolean state variable to launch the verification test of adding a new quote is no longer activated and therefore never goes to true (It should be set to true in inputHandler (the callback function of the input element in the return()) which then enables the first useEffect since the boolean state variable is in the dependency array.).

Does anyone know what can cause this behavior between an input, its callback function and the state variables?

If anyone understands my problem and is willing to help me it would be a great help.

CodePudding user response:

I didn't find the answer to my second question (stopping the feature from working if the user spams the input with a lot of characters) but I finally found my problem. I was missing a useEffect for my previous code to work normally, because of that, one of my state variables didn't have time to update and this creates the strange behaviors of repositioning the cursor in the input.

In case someone is interested I put the correct code here (I added some validation conditions in some useEffects to avoid bugs of randomly adding quotation marks especially when copying and pasting):

import * as React from "react";
import { useEffect, useState, useRef } from "react";
 
const QuoteInput: React.FC = () => {
 
    const inputRef = useRef<HTMLInputElement | null>(null);
 
    const [inputChoose, setInputChoose] = useState("");
    const [previousInputChoose, setPreviousInputChoose] = useState("");
 

    const [testQuoteAddition, setTestQuoteAddition] = useState(false);
    const [enterSingleQuote, setEnterSingleQuote] = useState(false);
    const [enterDoubleQuote, setEnterDoubleQuote] = useState(false);

    const [writeNewSingleQuote, setWriteNewSingleQuote] = useState(false);
    const [writeNewDoubleQuote, setWriteNewDoubleQuote] = useState(false);

    const [repositionCaretBetweenQuotes, setRepositionCaretBetweenQuotes] = useState(false);
    const [indexDifferenceInInput, setIndexDifferenceInInput] = useState(0);
 
    const inputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
        setPreviousRequestChoose(requestChoose);
        const enteredRequest = event.target.value;
        setRequestChoose(enteredRequest);
 
        setTestQuoteAddition(true);
    };

    useEffect(()=>{
        if(testQuoteAddition === true && (previousInputChoose.length === inputChoose.length || previousInputChoose.length === inputChoose.length - 1)) {
            for(let i=0; i<=inputChoose.length; i  ) {
                if(previousInputChoose.charAt(i) !== inputChoose.charAt(i))
                {
                    let previousSingleQuoteNumber = previousRequestChoose.split("'").length - 1;
                    let previousDoubleQuoteNumber = previousRequestChoose.split('"').length - 1;
                    let previousQuoteNumber: number;
                    if (previousSingleQuoteNumber !== undefined && previousDoubleQuoteNumber !== undefined) {
                        previousQuoteNumber = previousSingleQuoteNumber   previousDoubleQuoteNumber;
                    } else if (previousSingleQuoteNumber !== undefined && previousDoubleQuoteNumber === undefined) {
                        previousQuoteNumber = previousSingleQuoteNumber;
                    } else if (previousSingleQuoteNumber === undefined && previousDoubleQuoteNumber !== undefined) {
                        previousQuoteNumber = previousDoubleQuoteNumber;
                    } else {
                        previousQuoteNumber = 0;
                    }
                    previousQuoteNumber = previousSingleQuoteNumber!   previousDoubleQuoteNumber!;

                    let currentSingleQuoteNumber = requestChoose.split("'").length - 1;
                    let currentDoubleQuoteNumber = requestChoose.split('"').length - 1;
                    let currentQuoteNumber: number;
                    if (currentSingleQuoteNumber !== undefined && currentDoubleQuoteNumber !== undefined) {
                        currentQuoteNumber = currentSingleQuoteNumber   currentDoubleQuoteNumber;
                    } else if (currentSingleQuoteNumber !== undefined && currentDoubleQuoteNumber === undefined) {
                        currentQuoteNumber = currentSingleQuoteNumber;
                    } else if (currentSingleQuoteNumber === undefined && currentDoubleQuoteNumber !== undefined) {
                        currentQuoteNumber = currentDoubleQuoteNumber;
                    } else {
                        currentQuoteNumber = 0;
                    }
                    currentQuoteNumber = currentSingleQuoteNumber!   currentDoubleQuoteNumber!;

                    if (inputChoose.charAt(i) === "'" && currentQuoteNumber !== previousQuoteNumber) {
                        setEnterSingleQuote(true);
                    } else if (inputChoose.charAt(i) === '"' && currentQuoteNumber !== previousQuoteNumber) {
                        setEnterDoubleQuote(true);
                    }
                }
            }
        }
        setTestQuoteAddition(false);
    },[testQuoteAddition])

    useEffect(()=>{
        if(enterSingleQuote === true){
            let findDifference: boolean = false
            for(let i=0; i<inputChoose.length; i  ) {
                if(previousInputChoose.charAt(i) !== inputChoose.charAt(i) && findDifference === false)
                {
                    setIndexDifferenceInInput(i);
                    findDifference = true;
                }
            }
            setEnterSingleQuote(false);
            setWriteNewSingleQuote(true);
        } else if (enterDoubleQuote === true){
            let findDifference: boolean = false
            for(let i=0; i<inputChoose.length; i  ) {
                if(previousInputChoose.charAt(i) !== inputChoose.charAt(i) && findDifference === false)
                {
                    setIndexDifferenceInInput(i);
                    findDifference = true;
                }
            }
            setEnterDoubleQuote(false);
            setWriteNewDoubleQuote(true);
        }
    },[enterSingleQuote, enterDoubleQuote])

    useEffect(()=>{
        if (writeNewSingleQuote === true) {
            let newString: string = inputChoose.slice(0,indexDifferenceInInput   1)   "'"   inputChoose.slice(indexDifferenceInInput   1);
            setInputChoose(newString);

            if(inputRef !== null && inputRef !== undefined) {
                if (inputRef.current !== null && inputRef.current !== undefined) {
                    if (inputRef.current.setSelectionRange !== undefined) {
                        setRepositionCaretBetweenQuotes(true);
                    }
                }
            }
        } else if (writeNewDoubleQuote === true) {
            let newString: string = inputChoose.slice(0,indexDifferenceInInput   1)   '"'   inputChoose.slice(indexDifferenceInInput   1);
            setInputChoose(newString);

            if(inputRef !== null && inputRef !== undefined) {
                if (inputRef.current !== null && inputRef.current !== undefined) {
                    if (inputRef.current.setSelectionRange !== undefined) {
                        setRepositionCaretBetweenQuotes(true);
                    }
                }
            }
        }
    },[writeNewSingleQuote, writeNewDoubleQuote])

    useEffect(()=>{
        setWriteNewSingleQuote(false);
        setWriteNewDoubleQuote(false);
        if (repositionCaretBetweenQuotes === true) {
            inputRef?.current?.setSelectionRange(indexDifferenceInInput   1, indexDifferenceInInput   1);
        }
        setRepositionCaretBetweenQuotes(false);
    },[repositionCaretBetweenQuotes])
 
    return(
        <div>
            <input ref={inputRef} type="text" onChange={inputHandler} value={inputChoose} className="text-center" placeholder="enter an input" />
        </div>
    );
}
 
export default QuoteInput;
  • Related