Make writing a habit together! This is my first day to participate in the “Gold Digging Day New Plan · April More text challenge”, click to see the details of the activity.

background

Recently I received a request that a list appears when the $sign is entered in the password box. Click one of the items in the list to automatically fill the value in the password input box, and the input box should support plain ciphertext switching.

I immediately think of a scenario where typing @ in a chat text box will mention someone or something, and Antd’s Mentions component has this capability:

But you also need to support ciphertext switching. Nani? Mentions component doesn’t have this function! Does this require me to combine the password entry field with the functionality of the Mentions component? Er… No fishing today…

The target

Write an Input.Password component that combines the Mentions feature

Problems that need to be solved

  1. The input@The list of members whose symbols appear appears below the input box, and at a higher level. Click elsewhere on the page to see the list disappear.
  2. After clicking the target member, you need to dynamically change the value displayed in the input field, which involves the input fieldInterpolation at the cursorAs well asResets the cursor positionThe problem of
  3. supportSwitch the ciphertext. In ciphertext mode, the @ function is also supported. After clicking a member, the value displayed in the input box is ciphertext.

Knowledge points involved

  1. Ciphertext switch -> Use the type= “password” attribute in the native input
  2. Interpolation at the cursorAs well asResets the cursor position-> using dom elementsselectionEndwithsetSelectionRangemethods
  3. Click on the rest of the page to remove the member list -> use directlyahooksIn the databaseuseClickAwaymethods
  4. The list of members appears below the input box and is higher -> usedrc-trigger
  5. Asynchronous problem with setState updating values
  6. Controlled components and uncontrolled components
const dom = document.getElementById('id');
const idx = dom.selectionEnd; // Get the cursor position
dom.setSelectionRange(startIndex, endIndex);  // Customize the text range in the input box
Copy the code

Record on pit

1. Can’t you implement the Mentions component based on Antd?

I did try repackaging or rewriting the Mentions component directly based on Antd before I started writing the component, but found it too naive.

The author first gets the component instance mentions using ref:

Where, when we type in the page, we trigger the native onChange event for textarea in the mentions component, which triggers the internal triggerChange method that changes the textarea component value, that is, The internal Textarea is a controlled component.

Internal source code:

At this point, if I want to implement my requirements, I need to dynamically change the textarea component value, whereas the only method we can use for the mentions component is the triggerChange event, which we can use in the business code, The onChange event for the Mensions component itself is used to dynamically change the textarea value, so that it will be looped into an error.

2.Antd Mentions are implemented based on textarea, why should I replace it with input?

This is actually a question of whether to implement a password input box. I here because business needs more urgent, here will not go into depth to discuss how to achieve, directly use the native input password box. Friends who are interested or have ideas can leave comments.

3. SetSelectionRange method invalid? Can’t customize cursor position?

At first, the author used the state value to control the display value of the input field, making the input field become a controlled component. However, due to the delay in updating the useState data in React, when setSelectionRange is executed, the DOM element in the page has not been updated yet. When the setSelectionRange method is followed, the DOM element is updated, causing the element to be redrawn, which naturally invalidates the method.

The setSelectionRange has to be focused before it works.

const onSelect = (value: string) = > {
    // Cursor interpolation
    const selectionEndIdx = getSelectionEndIdx(); // Cursor position
    const newValue = inputText.slice(0, selectionEndIdx) + value + inputText.slice(selectionEndIdx)
    // setInputText(newValue); This is a controlled component. Use the next line of code to get rid of this problem
    inputRef.current.value = newValue;
    // Reset the mouse cursor position
    const idx = selectionEndIdx + value.length
    inputRef.current.focus();   SetSelectionRange = setSelectionRange = setSelectionRange
    inputRef.current.setSelectionRange(0.0)}Copy the code

4. How do I determine if a string starts with a substring?

const isTargetStart = strCode.indexOf("ssss"); 

// isTargetStart === 0 strCode starts with SSSS
// isTargetStart === -1 indicates that strCode does not start with SSSS
Copy the code

The final code

import { useClickAway } from "ahooks";
import React from 'react';
import Trigger from 'rc-trigger';
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';

const optionsBase = [
  { name: '1' },
  { name: '2'}]export const MentionsWithPassword = (): React.ReactElement => {
  const [showPwd, setShowPwd] = React.useState(true);
  const [popupVisible, setPopupVisible] = React.useState(false);
  const popupRef = React.useRef<any>(null);
  const inputRef = React.useRef<any>(null);
  const [visibleOption, setVisibleOptions] = React.useState(optionsBase);

  useClickAway(() = > {
    setPopupVisible(false)
  }, popupRef);

  const onPwdVisibleChange = () = >{ setShowPwd(! showPwd); }// Determine whether a reference argument list should appear
  const calcPopupVisible = (text: string) = > {
    const selectionEndIdx = getSelectionEndIdx(); // Cursor position
    const textBeforeSelectionEnd = text.substring(0, selectionEndIdx);  // The text before the cursor
    const lastIdx = textBeforeSelectionEnd.lastIndexOf('$');
    // 1. There is no matching character
    if (lastIdx === -1) return;
    // 2. Match the character at the cursor position
    if (lastIdx === textBeforeSelectionEnd.length - 1) {
      setPopupVisible(true);
      setVisibleOptions(optionsBase)
      return;
    }
    // 3. Not only matching characters are entered, but other characters are also entered
    const centerText = textBeforeSelectionEnd.substring(lastIdx + 1);
    if (centerText.includes(' ')) {
      setPopupVisible(false)
      return;
    }
    setPopupVisible(true);
    setVisibleOptions(getFilterOptions(centerText, options));
  }

  // Get the cursor position
  const getSelectionEndIdx = () = > {
    return inputRef.current.selectionEnd;
  }

  // Filter the reference parameter list
  const getFilterOptions = (text: string, options: any[]) = > {
    return options.filter(item= > item.name.indexOf(text) > -1)}const onChange = (e: any) = > {
    const value = e.target.value;
    calcPopupVisible(value);
  }
  const onSelect = (value: string) = > {
    // Cursor interpolation
    const selectionEndIdx = getSelectionEndIdx(); // Cursor position
    const oldInputText = inputRef.current.value;
    const newValue = oldInputText.slice(0, selectionEndIdx) + value + oldInputText.slice(selectionEndIdx)
    // setInputText(newValue);
    inputRef.current.value = newValue;
    // Reset the mouse cursor position
    const idx = selectionEndIdx + value.length
    inputRef.current.focus();
    inputRef.current.setSelectionRange(idx, idx);
  }

  return (
    <div>
      <Trigger
        popup={(
          <div ref={popupRef} style={{ background: 'red' }}>
            {visibleOption.map(item => <div onClick={()= > onSelect(item.name)}>{item.name}</div>)}
          </div>
        )}
        destroyPopupOnHide
        popupVisible={popupVisible}
        popupAlign={{
          points: ['tl', 'bl'],
          offset: [0, 3]
        }}
      >
        <input
          type={showPwd ? 'text' : 'password'}
          onChange={onChange}
          ref={inputRef}
        />
      </Trigger>
      <span onClick={onPwdVisibleChange}>{showPwd ? <EyeInvisibleOutlined /> : <EyeTwoTone />}</span>
    </div>)}Copy the code

Refer to the link

  1. HTMLInputElement.setSelectionRange() – MDN