ReactJS处理textarea中的制表符

时间:2016-10-30 17:39:43

标签: html reactjs

如何在ReactJS中处理Tab键按下的事件,以便我能够在textarea中缩进文本?

当在textarea上按下tab时,

onChange事件不会被触发,所以我猜可能有一个更高级别的处理程序可以用来检测这个事件。

3 个答案:

答案 0 :(得分:1)

您可以尝试onKeyDown并获取标签的密钥代码。

add: function(event){
    console.log(event.keyCode); //press TAB and get the keyCode
},
render: function(){
    return(
        <div>
            <input type="text" id="one" onKeyDown={this.add} />    
        </div>
    );
}

答案 1 :(得分:0)

onKeyDown={(e: React.KeyboardEvent) => {
  if (e.key === 'Tab' && !e.shiftKey) {
    e.preventDefault();
    const value = this.textareaRef.current!.value;
    const selectionStart = this.textareaRef.current!.selectionStart;
    const selectionEnd = this.textareaRef.current!.selectionEnd;
    this.textareaRef.current!.value =
      value.substring(0, selectionStart) + '  ' + value.substring(selectionEnd);
    this.textareaRef.current!.selectionStart = selectionEnd + 2 - (selectionEnd - selectionStart);
    this.textareaRef.current!.selectionEnd = selectionEnd + 2 - (selectionEnd - selectionStart);
  }
  if (e.key === 'Tab' && e.shiftKey) {
    e.preventDefault();
    const value = this.textareaRef.current!.value;
    const selectionStart = this.textareaRef.current!.selectionStart;
    const selectionEnd = this.textareaRef.current!.selectionEnd;

    const beforeStart = value
      .substring(0, selectionStart)
      .split('')
      .reverse()
      .join('');
    const indexOfTab = beforeStart.indexOf('  ');
    const indexOfNewline = beforeStart.indexOf('\n');

    if (indexOfTab !== -1 && indexOfTab < indexOfNewline) {
      this.textareaRef.current!.value =
        beforeStart
          .substring(indexOfTab + 2)
          .split('')
          .reverse()
          .join('') +
        beforeStart
          .substring(0, indexOfTab)
          .split('')
          .reverse()
          .join('') +
        value.substring(selectionEnd);

      this.textareaRef.current!.selectionStart = selectionStart - 2;
      this.textareaRef.current!.selectionEnd = selectionEnd - 2;
    }
  }
}}

答案 2 :(得分:0)

以防万一有人想要在 TypeScript 中稍微更新和(在我看来是增强的)vipe 解决方案的 React Hooks 版本:

实现用法示例:

<EnhancedTextArea
  ref={txtCodeInput} {/* reference used whenever required as seen below */}
  className='code-input'
  tabSize={2}
  onTextChange={handleCodeChange} {/* Function accepting callback of type (string) -> void, called every time code is changed */}
/>

获取文本:

const txtCodeInput = useRef<EnhancedTextAreaRefs>(null);
...
const codeContent = txtCodeInput.current?.getCodeContent();

EnhancedTextArea.tsx:

import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';

type EnhancedTextAreaProps = {
  onTextChange?: (text: string) => void,
  className?: string,
  spellCheck?: boolean,

  tabSize?: number,
};

export type EnhancedTextAreaRefs = {
  getCodeContent: () => string;
}

const EnhancedTextArea = forwardRef<EnhancedTextAreaRefs, EnhancedTextAreaProps>(({
  onTextChange = undefined,
  className = undefined,

  tabSize = 4,
  spellCheck = false,
}: EnhancedTextAreaProps, ref) => {
  const [text, setText] = useState('');
  const [stateSelectionStart, setStateSelectionStart] = useState(0);
  const [stateSelectionEnd, setStateSelectionEnd] = useState(0);

  const txtInput = useRef<HTMLTextAreaElement>(null);

  useImperativeHandle(ref, () => ({
    getCodeContent: () => text,
  }));
  
  useEffect(() => {
    const textArea = txtInput.current;

    if (!textArea) {
      return;
    }

    if (stateSelectionStart >= 0) {
      textArea.selectionStart = stateSelectionStart;
    }
    
    if (stateSelectionEnd >= 0) {
      textArea.selectionEnd = stateSelectionEnd;
    }
  }, [text, stateSelectionStart, stateSelectionEnd]);

  async function handleCodeChange(e: React.ChangeEvent<HTMLTextAreaElement>): Promise<void> {
    const text = e.target.value;

    setText(text);

    if (onTextChange) {
      onTextChange(text);
    }
  }
  
  async function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>): Promise<void> {
    const textArea = e.target as HTMLTextAreaElement;

    const tabString = ' '.repeat(tabSize);

    const value = textArea.value;
    const selectionStart = textArea.selectionStart;
    const selectionEnd = textArea.selectionEnd;

    if (e.key === 'Tab' && !e.shiftKey) {
      e.preventDefault();

      if (selectionStart !== selectionEnd) {
        const slices1 = getNewLineSlices(value, selectionStart, selectionEnd);
        const newValue1 = addTabs(value, slices1, tabString);

        setText(newValue1);
        setStateSelectionStart(selectionStart + tabSize);
        setStateSelectionEnd(selectionEnd + (newValue1.length - value.length));
      } else {
        const newValue2 = value.substring(0, selectionStart) + tabString + value.substring(selectionEnd);

        setText(newValue2);
        setStateSelectionStart(selectionEnd + tabSize - (selectionEnd - selectionStart));
        setStateSelectionEnd(selectionEnd + tabSize - (selectionEnd - selectionStart));
      }
    } else if (e.key === 'Tab' && e.shiftKey) {
      e.preventDefault();
  
      const slices2 = getNewLineSlices(value, selectionStart, selectionEnd);
      const newValue3 = removeTabs(value, slices2, tabSize);

      const diff = value.length - newValue3.length;

      setText(newValue3);
      setStateSelectionStart(Math.max(0, selectionStart - (diff ? tabSize : 0)));
      setStateSelectionEnd(Math.max(0, selectionEnd - diff));
    } else {
      setStateSelectionStart(-1);
      setStateSelectionEnd(-1);
    }
  }

  function getNewLineSlices(value: string, selectionStart: number, selectionEnd: number): Array<string | null> {
    const newLineLocations = getAllIndices(value, '\n');
    const left = findRange(newLineLocations, selectionStart);
    const split = value.split('\n');

    const arr = [];
    let count = 0;
    for (let i = 0; i < split.length; i++) {
      const line = split[i];

      if (count > left && count <= selectionEnd) {
        arr.push(line);
      } else {
        arr.push(null);
      }

      count += line.length + 1;
    }

    return arr;
  }

  function addTabs(value: string, arr: Array<string | null>, joiner: string): string {
    const split = value.split('\n');

    let ret = '';
    for (let i = 0; i < split.length; i++) {
      const val = split[i];
      const newLineVal = arr[i];
      
      if (newLineVal === val) {
        ret += joiner;
      }

      ret += val;
      if (i !== split.length - 1) {
        ret += '\n';
      }
    }

    return ret;
  }

  function removeTabs(value: string, arr: Array<string | null>, tabSize: number): string {
    const split = value.split('\n');

    let ret = '';
    for (let i = 0; i < split.length; i++) {
      const val = split[i];
      const newLineVal = arr[i];
      
      if (!val.startsWith(' ') || newLineVal !== val) {
        ret += val;
        if (i !== split.length - 1) {
          ret += '\n';
        }
        
        continue;
      }
    
      let count = 1;
      while (val[count] === ' ' && count < tabSize) {
        count++;
      }

      ret += val.substring(count);
      if (i !== split.length - 1) {
        ret += '\n';
      }
    }

    return ret;
  }

  function getAllIndices(arr: string, val: string): Array<number> {
    const indices = [];
    let i = -1;

    while ((i = arr.indexOf(val, i + 1)) !== -1){
      indices.push(i);
    }

    return indices;
  }

  function findRange(arr: Array<number>, min: number): number {
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] >= min) {
        return i === 0 ? -1 : arr[i - 1];
      }
    }

    return arr[arr.length - 1];
  }

  return(
    <textarea
      ref={txtInput}
      value={text}
      onKeyDown={handleKeyDown}
      onChange={handleCodeChange}

      className={className} 
      spellCheck={spellCheck} />
  );
});

EnhancedTextArea.displayName = 'EnhancedTextArea';

export default EnhancedTextArea;