background
The simulation realizes the function of Textarea. When the user enters @, a selection box will pop up to select the personnel. At the same time, the @ personnel will turn blue and insert into the editing box. The proposed solution to this requirement is to use the contenteditable=”true” of divs to allow them to input and listen for changes in the contents of divs, pop up a person Selection box when typing @, and use the property of the Selection object. Find the specific location of the mouse @ and insert the page content. The specific effects are as follows:
This paper recorded the problems encountered, ideas for your reference:
Preliminary knowledge
The Selection object
The getSelection() method is exposed to the Document and window objects and returns a Selection object for the currently selected text.
Const Selection = window.getSelection() // Const Selection = document.getSelection()Copy the code
GetSelection () is standardized in HTML5 and implemented in IE9 and all modern versions of Firefox, Safari, Chrome, and Opera (in short, IE8 and earlier are not compatible). The Selection object uses the following attributes: anchorNode: the node at the beginning of the Selection anchorOffset: In anchorNode, the number of characters skipped from the beginning to the beginning of the Selection focusNode: the node at the end of the Selection focusOffset: FocusNode contains more characters in the selection: developer.mozilla.org/zh-CN/docs/… When the region is not selected in the document, anchorNode and focusNode are the same node and the anchorOffset and focusOffset are the same, indicating the location of the mouse on the anchorNode node. To take advantage of this, when the user enters @, the current anchorNodeGlobal is used to record the node where the current mouse is located, and focusOffsetGlobal records the specific location of the node where the current mouse is located. This way, when inserting the blue tag, you know which node to insert it into.
Range object
// Create const range = document.createrange () // Select const range = Selection. GetRangeAt (0) Const Range = new Range() const Range = new Range();Copy the code
More properties: developer.mozilla.org/zh-CN/docs/…
Cursor to the end
Use the Selection and Range above to move the cursor to the end. In Vue, resetting the cursor position in the nextTick callback prevents modifying the Node Node from working. Method one:
function keepLastIndex(curDom: HTMLElement) {curdom.focus () const curSelection = window.getSelection() // create selection curSelection? .selectAllChildren(curDom) // Selection Selects all children of curDom? .collapsetoend () // cursor moved to end}Copy the code
Method 2:
function keepLastIndex(curDom: HTMLElement) {curdom.focus () // Solve the problem of ff not getting focus // current mouse pointer const curSelection = window.getSelection() // create an empty range Const rangeTmp = document. CreateRange () rangeTmp. SelectNodeContents (curDom) / / false fold to the end node, Rangetmp.collapse (false) curSelection? .removeallRanges () // Add to last curSelection? .addRange(rangeTmp) }Copy the code
The container DIV
contenteditable
<div contenteditable="true" class="edit-wrap"></div>
Copy the code
You can make DIV containers input.
Safari cursor The default value of Safari user-select is None, so this needs to be set to make contenteditable containers editable.
[contenteditable] {
-webkit-user-select: text;
user-select: text;
}
Copy the code
The DIV container only displays the edit cursor when it has content, so you can set the padding of the DIV container to 1px; Make the DIV container initialize with the edit cursor.
Placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder placeholder
<div class="o-input"> <div ref="editorRef" contenteditable="true" class="edit-wrap van-field__control" /> <div v-if="! editVal && showPlaceholder" contenteditable="false" class="edit-wrap-placeholder-o" >{{ placeholder }}</div> </div>Copy the code
Remove the DIV outline
.edit-wrap
&:focus
outline none
Copy the code
Gets the contents of the DIV
With editable divs, one of the key problems is getting the contents of the DIV, like the value property of a Textarea, to get the questions the user typed into the DIV. If you use the Node textContent directly, there will be a problem with the newline. The extra \n will cause the newline to display incorrectly. The main reason is that a
tag appears in a carriage return line feed in an editable DIV. Sometimes the < BR > tag may not be removed when deleted, which makes it incorrect to use textContent as the DIV value directly.
The correct solution is to loop through the child elements of DIV, append \n; If it is a BR element, check whether the element can be appended \n;
if (cur.tagName === 'BR' && (cur.nextSibling? .nodeName ! == 'DIV' && cur.nextSibling? .nodeName ! == 'P') && (cur.parentNode? .nodeName === 'DIV' || cur.parentNode? .nodeName === 'P') && (cur.parentNode? .nextSibling? .nodeName ! == 'DIV' && cur.parentNode? .nextSibling? .nodeName ! == 'P') ) { result += '\n' }Copy the code
If the element is of type nodeType = 1 and has child elements, loop through the child elements. Full code:
// let result = "" const getNodeText = (curNode: Node) => {if (curnode.nodeType === 1 && curnode.nodename! == 'BR') { if (curNode.nodeName === 'DIV' || curNode.nodeName === 'P') { result += '\n' } const len = curNode.childNodes.length for (let i = 0; i < len; i++) { const nodeItem = curNode.childNodes[i] getNodeText(nodeItem) } } else if (curNode.nodeType === 1 && curNode.nodeName === 'BR') { const cur = curNode as HTMLElement if (cur.tagName === 'BR' && (cur.nextSibling? .nodeName ! == 'DIV' && cur.nextSibling? .nodeName ! == 'P') && (cur.parentNode? .nodeName === 'DIV' || cur.parentNode? .nodeName === 'P') && (cur.parentNode? .nextSibling? .nodeName ! == 'DIV' && cur.parentNode? .nextSibling? .nodeName ! == 'P') ) { result += '\n' } else { result += cur.innerHTML } } else if (curNode.nodeType === 3) { result += CurNode. NodeValue}} / / div inside.childnodes export const getEditVal = (editDom: Node | null) : string = > {the if (editDom? .childNodes) { result = '' const len = editDom.childNodes.length for (let i = 0; i < len; i++) { const nodeItem = editDom.childNodes[i] getNodeText(nodeItem) } return result } return '' }Copy the code
Content of the escape
Because the @ person is displayed in blue, the text content of the editable DIV will be displayed using V-HTML, so the < and > HTML tags entered by the user should be escaped.
// export const funEncodeHTML = function (STR: string) { if (typeof str === 'string') { return str.replace(/<|&|>/g, (matches: string): string => ({ '<': '< ', '>': '> ', '&': '& '}) (matches) | | ' ')} return ' '} / / decoding export const funDecodeHTML = function (STR: string) { if (typeof str === 'string') { return str.replace(/< |> |& /g, (matches: string): string => ({ '< ': '<', '> ': '>', '& ': '&', })[matches] || '') } return '' }Copy the code
Listen for DIV content changes
There are two main events: listen for keyDown and input events on the editable DIV container in the order that keyDown => INPUT is the first keydown followed by the input event.
<div
ref="editorRef"
contenteditable="true"
class="edit-wrap van-field__control"
@input="editorInput"
@keydown="handleKeyDown"
/>
Copy the code
1. In Chrome 53, event. Data is not returned for input events and the currently entered value is not returned. The keyDown event records which key was pressed during the current input: curcodekey. value = e.key 2. In iPhone 8 Plus, the keyDown event will not be triggered if @ is typed in Chinese.
const editorInput = (e: If (e.ata &&e.ata === '@') {curcodeKey. value = e.ata} . }Copy the code
Listening input method
There are two main events: compositionStart and compositionEnd; Compositionstart indicates the start of Chinese input. Compositionend Indicates the end of Chinese input.
<div
ref="editorRef"
contenteditable="true"
@compositionstart="handleStart"
@compositionend="handleEnd"
/>
Copy the code
Stomp: CompositionStart and CompositionEnd events are not triggered on Android, and only handle the number limit in the input event and popup the selection box to select people when typing @.
Paste the limit
If the pasted text + DIV itself exceeds the maximum value of its own limit, there is a problem, so it needs to monitor the pasting event, when the number of words is too much to cut, paste only the maximum number of words:
<div
ref="editorRef"
contenteditable="true"
@paste="handlePast"
/>
Copy the code
const handlePast = (event: ClipboardEvent) => {// Decoder const realEditVal = funDecodeHTML(editVal.value) if (props. Max <= realEditVal.length) { Return} // Paste the text const paste = event.clipboardData? .getData('text') const toastNum = props.max - realEditVal.length const contentPast = paste? Substr (0, toastNum) | | 'const / / the current cursor position selection = window. The getSelection () if (! selection? RangeCount) return / / delete has selected text selection. DeleteFromDocument () const insertNodeTmp = Document.createtextnode (contentPast) Selection.getrangEat (0).insertNode(insertNodeTmp) // Move the cursor to after the text is copied selection.collapse(insertNodeTmp, Contentpast.length) // Save saveDivEnglish() if (paste && toastNum < paste?.length) {const tNum = math.max (0, // Prevent the default paste event.preventDefault()}Copy the code
Editing orientation
AnchorNode is the node in the loop container DIV. The cursor position can be found when the node is the same as the recorded anchorNode (===).
Pit: In Safari, const Selection = window.getSelection() is automatically updated as the cursor moves, so store Selection. AnchorNode after @.
Algorithm part
There are two main aspects: on the one hand, it monitors the input. When the @ symbol is entered, the selection box pops up, and the blue SPAN label is inserted into the position of @; On the other hand, control the number of words, when the number of words exceeds the maximum number of words can not be input, but for Chinese input is the process of insertion, if the number of text exceeds, delete the redundant text from the text behind, the effect is similar to the word limit Textarea.