I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!
I made a seemingly simple input association function before, but actually there are many pits, similar to the @selection function, today I will share the pit record with you.
Requirements describe
-
Text input box;
-
Enter the @ symbol / @ + keyword to associate the result list, which is displayed in the floating window at the cursor.
-
Click the list of associative results, replace the typed @ + associative keyword with selected associative results, and display the blue font;
-
After the association is triggered, if the association result is not selected, it is only used as plain text. If the association result is not selected, the association is ended by space.
-
Before submission, you can edit repeatedly, so you need to save the edited content and backfill display, the database stores HTML strings, convenient to repeatedly edit backfill display;
-
Save the edited content when you lose focus after editing; After completion and submission, it can not be edited, but can only be viewed. When viewing, more information will be displayed in the floating window when the mouse is hovering over the highlighted text.
-
Display Placeholder;
-
Length: a maximum of 200 characters (statistics of entered characters/total characters are displayed at the lower right corner of the text box).
-
All types of formats are acceptable, including numbers, Chinese and English, special characters, newlines, Spaces, etc.
Functional analysis
The function of limiting the maximum number of words in the rich text editor was not found in the research process, so it was abandoned.
So the contentEditable Div, with its customizable width and height and CSS style for internal nodes, seems to be mostly satisfying;
Other Placeholder features, real-time length, maximum length limits, and input listening all look like normal features.
The function development
The React + AntD project was developed.
structure
<div className={'editDivContentWrap'} ><div
contentEditable={true}
ref={editableDivRef}
placeholder={placeholder}
dangerouslySetInnerHTML={{ __html: htmlStr }}
onKeyUp = {handleEditableKeyUp}
onKeyPress = {handleEditableKeyPress}
onKeyDown = {handleEditableKeyDown}
onBlur = {handleEditBlur}
onMouseUp={handleEditableMouseUp}
onPaste={handleEditablePaste}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
/>
<div className={'textLength'} >
<span>{hasInputLength}</span>/
<span>{MAXLENGTH}</span>
</div>
{
showSearchResult &&
<div className={'searchResultTableWrap'} >
XXX
</div>
}
</div>
Copy the code
Record on pit
Placeholder
There were problems implementing the seemingly simplest Placeholder, crying…
contentEditable
div
enter
Type
and type English 1 instead of
;
When there’s nothing in the input box, the first time you type enter, you type
, because if you don’t type two newlines it shows up in the view as no newlines, and that’s what the browser does;
Try to use \n to implement newline, results in the following problem, so still choose the browser processing;
Enter
To prevent default typing, replace with\n
If there is no content in the input box, the page displays no line break when you type Enter for the first time. In The Chinese input method, two first letters are entered when you type Enter for the first time.
myPlaceholder
Eventually Placeholder implementation;
const handlePlaceholder = () = > {
// Place a placeholder in the edit box and press Enter, then delete
const htm = editableDivRef.current.innerHTML;
if(htm.length ! = =0&& htm ! = ='<div><br></div>') {
setMyPlaceholder(' ');
} else {
setMyPlaceholder('myPlaceholder');
if (htm === '<div><br></div>') {
editableDivRef.current.innerHTML = ' '; }}}Copy the code
- Lenovo floating window
Then there are the lenovo floating Windows, still with problems, crying…
There is no key control selection associative results, put in the next phase, really liver can not come over, thanks to the product greatly do not kill the grace;
- Click on the Associative result to replace it with highlighted text
Select @+ associative keyword and replace it with highlighted text.
Highlight text using Button+Css, highlight text as a whole processing, can only be deleted as a whole, not select and edit;
const itemClick = (record) = > {
// Hide the floating window
updateSearchFlag(false.null);
// Get the input to determine the maximum length
const txt = editableDivRef.current.textContent;
const length = txt.length;
// Select the text you want to use in the associative result to highlight it
const code = record.codeNum || ' ';
const addCodeLen = code.toString().length;
if (length > MAXLENGTH || (addCodeLen + length > MAXLENGTH)) {
// Input cannot continue beyond the maximum length
maxLengthTipFun();
return false;
} else {
// Update the real-time length
setHasInputLength(addCodeLen + 1 + length);
}
if (editableDivRef.current) {
editableDivRef.current.focus();
// delete the associative draft start
const editorRangeRange = editorRange.range;
if(! editorRangeRange)return;
// Get the last text node
const textNode = editorRangeRange.endContainer;
// Cursor position
const endOffset = editorRangeRange.endOffset;
// Locate the at symbol before the cursor
const textNodeValue = textNode.nodeValue;
const expRes = (/ @ $/ ([^ @] *)).exec(textNodeValue);
if (expRes && expRes.length > 1) {
editorRangeRange.setStart(textNode, expRes.index);
editorRangeRange.setEnd(textNode, endOffset);
editorRangeRange.deleteContents(); // Delete the associative draft end
// Use Button to highlight text
const btn = document.createElement('button');
btn.className = `createCodeNum`;
btn.textContent = ` @${code}`;
// Highlight text is not editable
btn.contentEditable = 'false';
// Prevents the cursor from switching to the inside of the highlighted text
btn.addEventListener('click'.() = > {
return false
}, false);
// Not selectable
btn.tabIndex = -1;
// Space characters for cursor placement
const bSpaceNode = document.createTextNode(' ');
insertHtmlAtCaret([btn, bSpaceNode], editorRange.selection, editorRange.range)
}
}
}
Copy the code
Cursor position updated after replacement;
This paragraph actually I also do not quite understand, have big guy to see also please don’t hesitate to give advice, thank!
const insertHtmlAtCaret = ([btn, bSpaceNode], selection, range) = > {
if (selection.getRangeAt && selection.rangeCount) {
if (selection.focusNode.parentNode.nodeName === 'BUTTON') return;
range.deleteContents();
const el = document.createElement("div");
el.appendChild(btn);
el.appendChild(bSpaceNode);
let frag = document.createDocumentFragment(), node, lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true); selection.removeAllRanges(); selection.addRange(range); }}}Copy the code
- Floating window click on the
There is a problem caused by the trigger timing of click and blur. When the floating window is displayed, click the floating window list item, the trigger sequence is to enter the box blur first and then trigger the click of the floating window list item, so the floating window list item cannot be clicked.
When the floating window is displayed, if you directly click other areas to lose focus, you will not save, instead, you should trigger save. You should be able to monitor click processing in document. After friendly communication with the product, you will click the page back button to save, leaving this pit.
const handleEditBlur = (e) = > {
// Blur is triggered first and itemClick is triggered later
setTimeout(() = > {
updateSearchFlag(false.null);
}, 300)
if (showSearchResult) {
// When the float window is displayed, Blur is triggered first and itemClick is triggered again. When the focus is lost, the interface save is not requested, because the focus is also lost when the float window is selected
// If you click on another area after the float window is displayed and lose focus, you will not save
} else {
// Get edit content, request interface save
if (editableDivRef.current.textContent.length > MAXLENGTH) {
handleBlur(sliceOverText());
} else{ handleBlur(editableDivRef.current.innerHTML); }}}Copy the code
- Floating window position calculation
Introducing the caret – pos;
const getResultPosition = () = > {
// Associated with the scroll displacement
// Relative to the outer element
// const pos = position(input); // { left: 15, top: 30, height: 20, pos: 15 }
// Relative view
// const off = offset(input); // { left: 15, top: 30, height: 20 }
const pos = position(editableDivRef.current);
const off = offset(editableDivRef.current);
/ /
const clientWidth = editableDivRef.current.clientWidth;
let toClientLeft = pos.left;
if (toClientLeft + TABLE_WIDTH >= clientWidth) {
toClientLeft = clientWidth - TABLE_WIDTH;
}
/ / up and down
const fixHeightGap = TITLE_HEIGHT + 2 + 15;
const clientHeight = document.scrollingElement.clientHeight;
const scrollTop = document.scrollingElement.scrollTop;
let toClientTop = pos.top;
if (off.top + TABLE_HEIGHT >= clientHeight + scrollTop) {
toClientTop = (TABLE_HEIGHT + off.height - fixHeightGap) * -1;
} else {
toClientTop = pos.top + fixHeightGap - scrollTop;
}
return {
left: toClientLeft,
top: toClientTop,
};
}
Copy the code
- Character length calculation
It is calculated when the user enters;
const validateKeyEvent = (e, callback) = > {
if(notHandleKey.indexOf(e.key) ! = = -1 || compositionFlag) {
// These keys are not processed, such as Shift
return;
}
const txt = editableDivRef.current.textContent;
const length = txt.length;
if (length >= MAXLENGTH && (canUseKey.indexOf(e.key) === -1)) {
// Cannot be blocked under Chinese input method
// Losing focus will result in undeletion
// editableDivRef.current.blur();
// Input cannot continue beyond the maximum length. Some keys do not block default events
// setIsOverMaxLength(true);
const evt = e || window.event;
evt.preventDefault();
return false;
} else {
// setIsOverMaxLength(false);
setHasInputLength(length);
if(canSearch) { callback(e); }}}Copy the code
Four events were monitored;
const handleEditableKeyUp = (e) = > {
validateKeyEvent(e, (e) = > {
//}}Copy the code
const handleEditableKeyPress = (e) = > {
validateKeyEvent(e, (e) = > {
//}}Copy the code
const handleEditableKeyDown = (e) = > {
validateKeyEvent(e, (e) = > {
//}}Copy the code
const handleEditableMouseUp = (e) = > {
validateKeyEvent(e, (e) = > {
//}}Copy the code
The historical input is calculated when the output is displayed.
// There are historical records when backfill after updating the length
useEffect(() = > {
if(htmlStr ! = =null) {
const tempDom = document.createElement('div');
tempDom.contentEditable = "true";
tempDom.innerHTML = htmlStr;
let len = tempDom.textContent.length;
setHasInputLength(len);
} else {
setHasInputLength(0);
}
}, [htmlStr]);
Copy the code
When I copy and paste, I calculate the length
const handleEditablePaste = (e) = > {
// Get the paste board content
// @ts-ignore
let pastedText = (e.clipboardData || window.clipboardData).getData('text');
const rangeInfo = getEditorRange();
setEditorRange(rangeInfo);
if (rangeInfo && pastedText) {
const selectStringLen = rangeInfo.range.toString().length;
if (pastedText.length + hasInputLength - selectStringLen > MAXLENGTH) {
maxLengthTipFun();
} else {
// Delete the selected content
rangeInfo.range.deleteContents();
// Insert the paste content
const newTextNode = document.createTextNode(pastedText);
const bSpaceNode = document.createTextNode(' '); insertHtmlAtCaret([newTextNode, bSpaceNode], rangeInfo.selection, rangeInfo.range); }}// Hide the floating window
updateSearchFlag(false.null);
e.preventDefault();
return false;
}
Copy the code
- Chinese input method length calculation
Due to the particularity of Chinese input method, it is necessary to judge whether Chinese input is over.
const handleCompositionEnd = (e) = > {
// Control the timing of search when Chinese input is entered
setCompositionFlag(false);
// Input in Chinese
let inputText = e.data;
const rangeInfo = getEditorRange();
setEditorRange(rangeInfo);
if (rangeInfo && inputText) {
const selectStringLen = rangeInfo.range.toString().length;
const afterEnterLen = inputText.length + hasInputLength - selectStringLen;
if (afterEnterLen > MAXLENGTH) {
const newHtmlStr = sliceOverText();
setTimeout(() = > {
editableDivRef.current.innerHTML = newHtmlStr;
// Cursor positioned to the end
const childNodesArr = [...editableDivRef.current.childNodes]
rangeInfo.range.setStartAfter(childNodesArr[childNodesArr.length - 1]);
});
e.preventDefault();
return false;
} else{ setHasInputLength(afterEnterLen); }}// Hide the floating window
updateSearchFlag(false.null);
}
Copy the code
const handleCompositionStart = (e) = > {
// Control the timing of search when Chinese input is entered
setCompositionFlag(true);
}
Copy the code
- Super long, block button after default event
When it is too long, blocking the default event of the key prevents further input. As a result, some other keys are blocked as well, such as up, down, left and right keys that move the cursor, copy and paste, etc.
So in the verification length, the part of the key to do processing;
// Enter the key k, e and y values that are still available when the maximum character is reached
const canUseKey = ['Backspace'.'ArrowLeft'.'ArrowRight'.'ArrowUp'.'ArrowDown'.'PageUp'.'PageDown'.'End'.'Home'];
// Do not handle key k, e, y values
const notHandleKey = ['Control'.'Shift'.'Alt'.'Unidentified'.'Escape'.'Tab'];
Copy the code
- Chinese input method
The timing problem of calculating character length and triggering search;
const handleCompositionStart = (e) = > {
// Control the timing of search when Chinese input is entered
setCompositionFlag(true);
}
Copy the code
const handleCompositionEnd = (e) = > {
// Control the timing of search when Chinese input is entered
setCompositionFlag(false); . }Copy the code
const handleSearchKey = (searchStr, rangeInfo) = > {
if(! compositionFlag) {// Search after Chinese input is completegetSearch && getSearch(key_words); }}Copy the code
- Copy and paste
Copy and paste when replacing elements, cursor position processing;
See the previous length calculation when copying and pasting;
- View preview of input completed
View the preview HTML string, get highlighted text when the mouse hover, get more information through the interface, used for floating window display;
Solution: one is converted to node by child element processing, another is processed by string;
I chose string handling because it’s easy;
const handleHtmlStr = (htmlStr, contentType) = > {
if (htmlStr === null) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} style={{ margin: '5px 0px 0' }} />;
}
// Process strings
const codeNumStart = htmlStr.split('<button class="createCodeNum" contenteditable="false" tabindex="-1">');
const result = codeNumStart.map((element, index) = > {
if (element) {
if (element.indexOf('</button>') > -1) {
const codeNumArr = element.split('</button>');
const codeNumArr2 = codeNumArr[0].split(The '@');
const codeNum = codeNumArr2[1];
let codeInfo = ' ';
new Promise(async() = > {await api.queryCodeInfo(codeNum).then((res) = > {
if (res && res.status === 200 && res.data) {
codeInfo = `... `;
}
})
.catch((err) = > {
console.error(err);
})
.finally(() = >{})});return (
<Fragment>
<Tooltip
overlayStyle={{
maxWidth: '380px',
fontSize: '12px'}}title={()= > { return codeInfo }}
>
<button className="createCodeNum">{`@${codeNum}`}</button>
</Tooltip>
<span dangerouslySetInnerHTML={{ __html: codeNumArr[1] }}></span>
</Fragment>
);
} else {
return <span dangerouslySetInnerHTML={{ __html: element}} ></span>; }}else {
return ' '; }});return result;
}
Copy the code
conclusion
That’s all there is to it, if you encounter a similar need, please try to be friendly and negotiate a new solution with the product, such as AntD mentions is also good, if communication fails, assess the difficulty and complexity of the task carefully, otherwise…
Of course, if the big guy saw here, but also hope to give advice, the front road is long, let’s look together.