Как остановить курсор от перехода к концу ввода - PullRequest
5 голосов
/ 16 апреля 2020

У меня есть контролируемый компонент ввода 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 });

}

Я начинаю сталкиваться с проблемой, когда пытаюсь удалить / добавить ди git где-то посередине, а затем курсор сразу перемещается в конец строки.

Я видел решение на решение от Соф ie, но я думаю, что здесь это не применимо как setState в любом случае вызовет рендеринг. Я также пытался манипулировать положением каретки с помощью setSelectionRange (start, end), но это тоже не помогло. Я думаю, что setState, который вызывает рендеринг, заставляет компонент обрабатывать отредактированное значение как конечное значение и заставляет курсор перемещаться в конец.

Может кто-нибудь помочь мне разобраться, как решить эту проблему?

Ответы [ 4 ]

3 голосов
/ 24 апреля 2020

onChange одного будет недостаточно.

Случай 1: Если target.value === 123|456, то вы не знаете, как '-' был удален. С <del> или с <backspace>. Таким образом, вы не знаете, должны ли итоговое значение и позиция каретки быть 12|4-56 или 123-|56.

Но что если вы сохраните предыдущую позицию и значение каретки? Допустим, в предыдущем onChange у вас было

123-|456

, а теперь у вас есть

123|456

, что, очевидно, означает, что пользователь нажал <backspace>. Но тут наступает ...

Случай 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 Но способ перемещения каретки может не соответствовать вашему вкусу. В любом случае, это, вероятно, не единственная библиотека.

2 голосов
/ 21 апреля 2020

Решение , которое вы пробовали , должно работать.

Обратите внимание, что - при реакции состояние обновляется асинхронно. Чтобы сделать то, что вам нужно сделать, как только обновления состояния будут выполнены, используйте 2-й аргумент setState.

Согласно docs

Второй параметр для setState () - это дополнительная функция обратного вызова, которая будет выполнена после завершения setState и повторной визуализации компонента.

Так что просто напишите встроенную функцию для выполнения setSelectionRange и передайте ее в качестве 2-го аргумента 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

2 голосов
/ 20 апреля 2020

Я боюсь, что если вы отмените sh элемент управления React, то неизбежно, что изменение состояния отменит позицию каретки и, следовательно, единственное решение - справиться с этим самостоятельно.

Кроме того, сохраняя «текущая позиция» с учетом ваших манипуляций со строками не такая уж тривиальная ...

Чтобы попытаться лучше разобраться в проблеме, я раскрутил решение с реагирующими хуками, где вы можете лучше увидеть, какие изменения состояния происходят

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

ссылка на codepen

Надеюсь, это поможет

1 голос
/ 26 апреля 2020

Сохраняя положение курсора в начале обработчика и восстанавливая его после отображения нового состояния, положение курсора всегда будет в правильном положении.

Однако, поскольку добавление - изменит положение курсора, ему необходимо чтобы рассмотреть его влияние на начальную позицию

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

...