简体   繁体   English

如何阻止 cursor 跳到输入结束

[英]How to stop cursor from jumping to the end of input

I have a controlled React input component and I am formatting the input as shown in onChange code.我有一个受控的 React 输入组件,我正在格式化输入,如 onChange 代码中所示。

<input type="TEL" id="applicantCellPhone" onChange={this.formatPhone} name="applicant.cellPhone" value={this.state["applicant.cellPhone"]}/>

And then my formatPhone function is like this然后我的formatPhone function 是这样的

formatPhone(changeEvent) {
let val = changeEvent.target.value;
let r = /(\D+)/g,
  first3 = "",
  next3 = "",
  last4 = "";
val = val.replace(r, "");
if (val.length > 0) {
  first3 = val.substr(0, 3);
  next3 = val.substr(3, 3);
  last4 = val.substr(6, 4);
  if (val.length > 6) {
    this.setState({ [changeEvent.target.name]: first3 + "-" + next3 + "-" + last4 });
  } else if (val.length > 3) {
    this.setState({ [changeEvent.target.name]: first3 + "-" + next3 });
  } else if (val.length < 4) {
    this.setState({ [changeEvent.target.name]: first3 });
  }
} else this.setState({ [changeEvent.target.name]: val });

} }

I start facing the problem when I try to delete/add a digit somewhere in the middle and then cursor immediately moves to the end of the string.当我尝试在中间某处删除/添加一个数字然后 cursor 立即移动到字符串的末尾时,我开始面临这个问题。

I saw a solution at solution by Sophie , but I think that doesn't apply here as setState will cause render anyways.我在Sophie的解决方案中看到了一个解决方案,但我认为这不适用于这里,因为 setState 无论如何都会导致渲染。 I also tried to manipulate caret position by setSelectionRange(start, end), but that didn't help either.我还尝试通过 setSelectionRange(start, end) 操作插入符号 position,但这也无济于事。 I think setState that causes render is making the component treat the edited value as final value and causing cursor to move to the end.我认为导致渲染的 setState 使组件将编辑的值视为最终值并导致 cursor 移动到最后。

Can anyone help me figuring out how to fix this problem?谁能帮我弄清楚如何解决这个问题?

onChange alone won't be enough.onChange是不够的。

Case 1: If target.value === 123|456 then you don't know how '-' was deleted.案例 1:如果target.value === 123|456那么你不知道'-'是如何被删除的。 With <del> or with <backspace> .使用<del>或使用<backspace> So you don't know should the resulting value and caret position be 12|4-56 or 123-|56 .所以你不知道结果值和插入符号 position 应该是12|4-56还是123-|56

But what if you'll save previous caret position and value?但是,如果您要保存以前的插入符号 position 和值怎么办? Let's say that on previous onChange you had假设在之前的onChange你有

123-|456

and now you have现在你有了

123|456

that obviously means that user pressed <backspace> .这显然意味着用户按下了<backspace> But here comes...但是来了...

Case 2: Users can change the cursor position with a mouse.案例2:用户可以用鼠标改变cursor position。

onKeyDown for the rescue: onKeyDown进行救援:

function App() {

  const [value, setValue] = React.useState("")

  // to distinguish <del> from <backspace>
  const [key, setKey] = React.useState(undefined)

  function formatPhone(event) {
    const element = event.target
    let   caret   = element.selectionStart
    let   value   = element.value.split("")

    // sorry for magical numbers
    // update value and caret around delimiters
    if( (caret === 4 || caret === 8) && key !== "Delete" && key !== "Backspace" ) {
      caret++
    } else if( (caret === 3 || caret === 7) && key === "Backspace" ) {
      value.splice(caret-1,1)
      caret--
    } else if( (caret === 3 || caret === 7) && key === "Delete" ) {
      value.splice(caret,1);
    }

    // update caret for non-digits
    if( key.length === 1 && /[^0-9]/.test(key) ) caret--

    value = value.join("")
      // remove everithing except digits
      .replace(/[^0-9]+/g, "")
      // limit input to 10 digits
      .replace(/(.{10}).*$/,"$1")
      // insert "-" between groups of digits
      .replace(/^(.?.?.?)(.?.?.?)(.?.?.?.?)$/, "$1-$2-$3")
      // remove exescive "-" at the end
      .replace(/-*$/,"")

    setValue(value);

    // "setTimeout" to update caret after setValue
    window.requestAnimationFrame(() => {
      element.setSelectionRange(caret,caret)
    })
  }  
  return (
    <form autocomplete="off">
      <label for="Phone">Phone: </label>
      <input id="Phone" onChange={formatPhone} onKeyDown={event => setKey(event.key)} name="Phone" value={value}/>
    </form>
  )
}

codesandbox 密码箱

You may also be interested in some library for the task.您可能还对该任务的某些库感兴趣。 There is for example https://github.com/nosir/cleave.js But the way it moves the caret may not be up to your taste.例如https://github.com/nosir/cleave.js但它移动插入符号的方式可能不符合您的口味。 Anyway, it's probably not the only library out there.无论如何,它可能不是唯一的图书馆。

I am afraid that given you relinquish the control to React it's unavoidable that a change of state discards the caret position and hence the only solution is to handle it yourself.恐怕如果您放弃对 React 的控制,state 的更改将不可避免地丢弃插入符号 position,因此唯一的解决方案是自己处理。

On top of it preserving the "current position" given your string manipulation is not that trivial...鉴于您的字符串操作并不是那么简单,因此保留“当前位置”最重要的是......

To try and better break down the problem I spinned up a solution with react hooks where you can better see which state changes take place为了尝试更好地解决问题,我使用 react hooks 构建了一个解决方案,您可以在其中更好地查看发生了哪些 state 更改

function App() {

  const [state, setState] = React.useState({});
  const inputRef = React.useRef(null);
  const [selectionStart, setSelectionStart] = React.useState(0);

  function formatPhone(changeEvent) {

    let r = /(\D+)/g, first3 = "", next3 = "", last4 = "";
    let old = changeEvent.target.value;
    let val = changeEvent.target.value.replace(r, "");

    if (val.length > 0) {
      first3 = val.substr(0, 3);
      next3 = val.substr(3, 3);
      last4 = val.substr(6, 4);
      if (val.length > 6) {
        val = first3 + "-" + next3 + "-" + last4;
      } else if (val.length > 3) {
        val = first3 + "-" + next3;
      } else if (val.length < 4) {
        val = first3;
      }
    }

    setState({ [changeEvent.target.name]: val });

    let ss = 0;
    while (ss<val.length) {
      if (old.charAt(ss)!==val.charAt(ss)) {
        if (val.charAt(ss)==='-') {
            ss+=2;
        }
        break;
      }
      ss+=1;
    }

    setSelectionStart(ss);
  }  

  React.useEffect(function () {
    const cp = selectionStart;
    inputRef.current.setSelectionRange(cp, cp);
  });

  return (
    <form autocomplete="off">
      <label for="cellPhone">Cell Phone: </label>
      <input id="cellPhone" ref={inputRef} onChange={formatPhone} name="cellPhone" value={state.cellPhone}/>
    </form>
  )  
}

ReactDOM.render(<App />, document.getElementById('root'))

link to codepen链接到代码笔

I hope it helps我希望它有帮助

The solution you tried should work. 您尝试的解决方案应该有效。

Note that - In react, state is updated in asynchronously.请注意 - 在反应中,state 是异步更新的。 To do the stuff you need to do as soon as the state updates are done, make use of 2nd argument of setState .要在 state 更新完成后立即执行您需要执行的操作,请使用setState的第二个参数。

As per docs根据文档

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. setState() 的第二个参数是一个可选的回调 function,它将在 setState 完成并重新渲染组件后执行。

So just write an inline function to do setSelectionRange and pass it as 2nd argument to setState所以只需编写一个内联 function 来执行setSelectionRange并将其作为第二个参数传递给setState

Like this像这样

...
this.setState({
    [changeEvent.target.name]: first3 + "-" + next3 + "-" + last4
},
    () => changeEvent.target.setSelectionRange(caretStart, caretEnd)
);
...

Working copy of the code is here:代码的工作副本在这里:

https://codesandbox.io/s/input-cursor-issue-4b7yg?file=/src/App.js https://codesandbox.io/s/input-cursor-issue-4b7yg?file=/src/App.js

By saving cursor position in the beginning of the handler and restoring it after new state rendered, cursor position will always be in correct position. By saving cursor position in the beginning of the handler and restoring it after new state rendered, cursor position will always be in correct position.

However, because adding - will change cursor position, it needs to considered its effect on initial position但是,因为添加-会改变 cursor position,所以需要考虑它对初始 position 的影响

import React, { useRef, useState, useLayoutEffect } from "react";

export default function App() {
  const [state, setState] = useState({ phone: "" });
  const cursorPos = useRef(null);
  const inputRef = useRef(null);
  const keyIsDelete = useRef(false);

  const handleChange = e => {
    cursorPos.current = e.target.selectionStart;
    let val = e.target.value;
    cursorPos.current -= (
      val.slice(0, cursorPos.current).match(/-/g) || []
    ).length;
    let r = /(\D+)/g,
      first3 = "",
      next3 = "",
      last4 = "";
    val = val.replace(r, "");
    let newValue;
    if (val.length > 0) {
      first3 = val.substr(0, 3);
      next3 = val.substr(3, 3);
      last4 = val.substr(6, 4);
      if (val.length > 6) {
        newValue = first3 + "-" + next3 + "-" + last4;
      } else if (val.length > 3) {
        newValue = first3 + "-" + next3;
      } else if (val.length < 4) {
        newValue = first3;
      }
    } else newValue = val;
    setState({ phone: newValue });
    for (let i = 0; i < cursorPos.current; ++i) {
      if (newValue[i] === "-") {
        ++cursorPos.current;
      }
    }
    if (newValue[cursorPos.current] === "-" && keyIsDelete.current) {
      cursorPos.current++;
    }
  };

  const handleKeyDown = e => {
    const allowedKeys = [
      "Delete",
      "ArrowLeft",
      "ArrowRight",
      "Backspace",
      "Home",
      "End",
      "Enter",
      "Tab"
    ];
    if (e.key === "Delete") {
      keyIsDelete.current = true;
    } else {
      keyIsDelete.current = false;
    }
    if ("0123456789".includes(e.key) || allowedKeys.includes(e.key)) {
    } else {
      e.preventDefault();
    }
  };

  useLayoutEffect(() => {
    if (inputRef.current) {
      inputRef.current.selectionStart = cursorPos.current;
      inputRef.current.selectionEnd = cursorPos.current;
    }
  });

  return (
    <div className="App">
      <input
        ref={inputRef}
        type="text"
        value={state.phone}
        placeholder="phone"
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
}

In above code these part will save position:在上面的代码中,这些部分将保存 position:

    cursorPos.current = e.target.selectionStart;
    let val = e.target.value;
    cursorPos.current -= (
      val.slice(0, cursorPos.current).match(/-/g) || []
    ).length;

And these will restore it:这些将恢复它:

    for (let i = 0; i < cursorPos.current; ++i) {
      if (newValue[i] === "-") {
        ++cursorPos.current;
      }
    }

Also a subtle thing is there, by using useState({phone:""}) we make sure input would re-render because it always set a new object.还有一个微妙的地方,通过使用useState({phone:""})我们确保输入会重新渲染,因为它总是设置一个新的 object。

CodeSandbox example is https://codesandbox.io/s/tel-formating-m1cg2?file=/src/App.js CodeSandbox 示例是https://codesandbox.io/s/tel-formating-m1cg2?file=/src/App.js

You can simply add the following lines in your formatPhone function--- --> if (.(event.keyCode == 8 || event.keyCode == 37 || event.keyCode == 39))--->您可以在 formatPhone 函数中简单地添加以下行--- --> if (.(event.keyCode == 8 || event.keyCode == 37 || event.keyCode == 39))--->

add this if condition to whole code written in formatPhone function.将此 if 条件添加到以 formatPhone function 编写的整个代码中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM