簡體   English   中英

如何阻止 cursor 跳到輸入結束

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

我有一個受控的 React 輸入組件,我正在格式化輸入,如 onChange 代碼中所示。

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

然后我的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 });

}

當我嘗試在中間某處刪除/添加一個數字然后 cursor 立即移動到字符串的末尾時,我開始面臨這個問題。

我在Sophie的解決方案中看到了一個解決方案,但我認為這不適用於這里,因為 setState 無論如何都會導致渲染。 我還嘗試通過 setSelectionRange(start, end) 操作插入符號 position,但這也無濟於事。 我認為導致渲染的 setState 使組件將編輯的值視為最終值並導致 cursor 移動到最后。

誰能幫我弄清楚如何解決這個問題?

onChange是不夠的。

案例 1:如果target.value === 123|456那么你不知道'-'是如何被刪除的。 使用<del>或使用<backspace> 所以你不知道結果值和插入符號 position 應該是12|4-56還是123-|56

但是,如果您要保存以前的插入符號 position 和值怎么辦? 假設在之前的onChange你有

123-|456

現在你有了

123|456

這顯然意味着用戶按下了<backspace> 但是來了...

案例2:用戶可以用鼠標改變cursor position。

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

密碼箱

您可能還對該任務的某些庫感興趣。 例如https://github.com/nosir/cleave.js但它移動插入符號的方式可能不符合您的口味。 無論如何,它可能不是唯一的圖書館。

恐怕如果您放棄對 React 的控制,state 的更改將不可避免地丟棄插入符號 position,因此唯一的解決方案是自己處理。

鑒於您的字符串操作並不是那么簡單,因此保留“當前位置”最重要的是......

為了嘗試更好地解決問題,我使用 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'))

鏈接到代碼筆

我希望它有幫助

您嘗試的解決方案應該有效。

請注意 - 在反應中,state 是異步更新的。 要在 state 更新完成后立即執行您需要執行的操作,請使用setState的第二個參數。

根據文檔

setState() 的第二個參數是一個可選的回調 function,它將在 setState 完成並重新渲染組件后執行。

所以只需編寫一個內聯 function 來執行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

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.

但是,因為添加-會改變 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>
  );
}

在上面的代碼中,這些部分將保存 position:

    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:""})我們確保輸入會重新渲染,因為它總是設置一個新的 object。

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

您可以在 formatPhone 函數中簡單地添加以下行--- --> if (.(event.keyCode == 8 || event.keyCode == 37 || event.keyCode == 39))--->

將此 if 條件添加到以 formatPhone function 編寫的整個代碼中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM