【问题标题】:How to stop cursor from jumping to the end of input如何阻止光标跳到输入的末尾
【发布时间】:2020-07-29 02:25:15
【问题描述】:

我有一个受控的 React 输入组件,我正在格式化输入,如 onChange 代码所示。

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

然后我的formatPhone函数是这样的

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

}

当我尝试在中间某处删除/添加一个数字然后光标立即移动到字符串的末尾时,我开始面临问题。

我在solution by Sophie 看到了一个解决方案,但我认为这不适用于这里,因为 setState 无论如何都会导致渲染。我还尝试通过 setSelectionRange(start, end) 来操纵插入符号的位置,但这也无济于事。 我认为导致渲染的 setState 使组件将编辑的值视为最终值并导致光标移动到末尾。

谁能帮我弄清楚如何解决这个问题?

【问题讨论】:

    标签: javascript reactjs cursor-position controlled-component


    【解决方案1】:

    onChange 是不够的。

    案例1:如果target.value === 123|456,那么你不知道'-'是如何被删除的。使用&lt;del&gt;&lt;backspace&gt;。所以你不知道结果值和插入符号位置应该是12|4-56 还是123-|56

    但是,如果您要保存 previous 插入符号的位置和值怎么办? 假设在之前的onChange 上你有

    123-|456

    现在你有了

    123|456

    这显然意味着用户按下了&lt;backspace&gt;。但是来了……

    案例2:用户可以通过鼠标改变光标位置。

    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

    您可能还对该任务的某些库感兴趣。例如https://github.com/nosir/cleave.js 但它移动插入符号的方式可能不符合您的口味。无论如何,它可能不是唯一的图书馆。

    【讨论】:

    • 如果输入123-456然后删除4光标将在-但如果再次按退格键将删除3
    • 谢谢@x00。您的解决方案似乎涵盖了很多东西,但我注意到的一件事是,即使达到最大长度,它也会让我不断输入数字。例如。如果我有 123-456-7890 并且我在 1 和 2 之间输入“1”,它可以让我这样做,新号码变为 112-345-6789。是的,还有一个看起来很有希望的库 (npmjs.com/package/react-number-format)。
    • 如果您更喜欢这种方式,那么onKeyDown = {event =&gt; { if(event.key.length === 1 &amp;&amp; (/[^0-9]/.test(event.key) || event.target.value.length &gt;= 12)) event.preventDefault(); else setKey(event.key) } 可以解决问题。 (另外/[^0-9]/check最好移到onKeyDown
    • @Makan,我就是这样写的。因为它没有指定它应该如何。解决方案有很多可能的调整。所有这些都是主观的。主要思想是onChange不够
    • @x00 我同意你的想法,onChange 是不够的。实际上,您的方式是一个很好的解决方案。我说的是从用户体验的角度来看的,但这没什么大不了的。
    【解决方案2】:

    我担心,如果你放弃对 React 的控制,状态更改将不可避免地丢弃插入符号的位置,因此唯一的解决方案是自己处理它。

    鉴于您的字符串操作不是那么简单,因此保留“当前位置”...

    为了尝试更好地解决问题,我使用 react hooks 提出了一个解决方案 在那里你可以更好地看到发生了哪些状态变化

    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

    希望对你有帮助

    【讨论】:

    • 感谢@user1602937 的回复。 “唯一的解决方案是自己处理”,你的意思是像 setSelectionRange?我也尝试过。我看到你也在你的代码中使用了它,但我仍然在你的 codepen 示例中看到了这个问题。还是你自己处理的意思是别的?
    • 通过处理它,我的意思是想出一个逻辑来检测修改时光标的位置。我的草稿有点错误,所以我更新了逻辑,但您可能希望对其进行改进以检测按键是否删除,并在这种情况下将光标向左移动一个。现在再看看我的 codepen,看看它是否更适合你。
    • 我添加了 onKeyDown 并检测到按键,这确实有助于处理删除场景。但是,这不再是问题了。问题是当我在字符串中间的某处删除或添加一个新数字时,光标会向右跳几个地方。我会想办法解决这个问题。
    【解决方案3】:

    解决方案you tried 应该可以工作。

    请注意 - 在 react 中,状态是异步更新的。要在状态更新完成后立即执行您需要执行的操作,请使用 setState 的第二个参数。

    根据docs

    setState() 的第二个参数是一个可选的回调函数,一旦 setState 完成并重新渲染组件,就会执行该回调函数。

    所以只需编写一个内联函数来执行setSelectionRange 并将其作为第二个参数传递给setState

    像这样

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

    代码的工作副本在这里:

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

    【讨论】:

    • 谢谢@gdh。我尝试了您的 codepen 示例,但我在那里看到了一个问题。例如。当我输入“1234”时,它会将其格式化为“123-4”,这很好,但光标会转到“4”之前的位置,因此下一个数字变为“123-54”而不是“123-45”。因此,我将代码中的范围增加了 +1,即 setSelectionRange(caretStart+1, caretEnd+1)。这有助于向前输入,但是当我在中间添加/删除数字时,光标再次开始出现异常。例如。如果我想从 123-456-7890 中删除“45”,我最终会得到 123-478-90
    【解决方案4】:

    通过将光标位置保存在处理程序的开头并在呈现新状态后恢复它,光标位置将始终处于正确位置。

    但是,由于添加-会改变光标位置,所以需要考虑它对初始位置的影响

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

    在上面的代码中,这些部分将保存位置:

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

    这些将恢复它:

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

    还有一个微妙的地方,通过使用useState({phone:""}),我们确保输入会重新渲染,因为它总是设置一个新对象。

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

    【讨论】:

    • 谢谢@Makan。在测试您的 codepen 解决方案时,我在输入非数字字符时发现了一个小问题。如果我有数字 123-456 并输入“a”或 1 到 2 之间的任何其他字母,它会将光标位置向右移动一个。
    • @abhi 你是对的。感谢您对细节的关注。我认为它现在已修复,希望可以正常工作!
    【解决方案5】:

    您可以在 formatPhone 函数中简单地添加以下行

    if (!(event.keyCode == 8 || event.keyCode == 37 || event.keyCode == 39))
    

    将此 if 条件添加到用 formatPhone 函数编写的整个代码中。

    【讨论】:

      猜你喜欢
      • 2012-01-03
      • 1970-01-01
      • 1970-01-01
      • 2019-08-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-21
      • 2011-11-08
      相关资源
      最近更新 更多