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
- 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. - After clicking the target member, you need to dynamically change the value displayed in the input field, which involves the input field
Interpolation at the cursor
As well asResets the cursor position
The problem of - support
Switch 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
- Ciphertext switch -> Use the type= “password” attribute in the native input
Interpolation at the cursor
As well asResets the cursor position
-> using dom elementsselectionEnd
withsetSelectionRange
methods- Click on the rest of the page to remove the member list -> use directly
ahooks
In the databaseuseClickAway
methods - The list of members appears below the input box and is higher -> used
rc-trigger
- Asynchronous problem with setState updating values
- 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
- HTMLInputElement.setSelectionRange() – MDN