Objective
When browsing the Web, you can occasionally see the selection of text can be shared or buttons, this is to use range with selection to achieve, selection is the current selection, converted to a Range object can be in the range of fragments within a series of operations, For example, to get the text content within the selection and so on.
Range and its use
In short, range is any piece of content in the HTML (fragment), which allows you to get any part of the HTML.
Range has several basic properties to reflect the scope of the current content, see.
Startoffset and Endoffset, starting with 0, contain startoffset and do not contain endoffset.
Range has a range (I am the standard), Microsoft TextRange (I know is IE dedicated, IE8 and below), selection and range compatibility related issues do not introduce here. So let's start with a simple familiarity with the API.
Get Range Object
// 从selection获取range var selection = window.getSelection();var range = selection.getRangeAt(0// 创建rangevar// >=ie9varnew// not ie
Setting range Ranges
// 设置起止位置 另外还有setStartBefore/setStartAfter和setEndBefore/setEndAfterrange.setStart(startContainer, startOffset);range.setEnd(EndContainer, endOffset);// 设置边界为referenceNode的起止位置(包含referenceNode)range.selectNode(referenceNode);// 设置边界为referenceNode的起止位置(不包含referenceNode)range.selectNodeContents(referenceNode);// 将边界进行折叠 toStart参数表示是否向前(起始边界)折叠range.collapse(toStart);
Working with range content (GET, delete, insert)
// 抽取html内容为fragment-将从页面中移除该范围内的片段range.extractContents();// 获取文本内容range.toString()// 删除内容range.deleteContents();// 在range起始位置插入一个nodevar textNode = document.createTextNode(‘new node‘);range.insertNode(textNode);
The specific API describes the visible MDN, which will be developed using the rangy library, which handles browser compatibility issues (mainly older versions of IE) and provides additional APIs and various plugins for easy development.
@ The implementation of the function
The @ function can be broadly divided into two situations, one is real-time input, the result panel appears at the bottom right of the cursor, and the other is to open a new selection page in which to complete the selection.
Here is the second, there are three main stages:
Trigger Selection -Two ways to enter @ Trigger and click Button Trigger
make selection -call other components to complete, return to select People list
finish the selection -Find the insertion point, insert the People tab
Phase 2 is ignored because it is called an external component. The other two phases are related to the insertion point of the character tag (bookmark), then the change of the bookmark is more critical. There are two time to save the bookmark (for each of the two ways of triggering the selection), enter @ Save the bookmark and lose focus to save the bookmark.
Trigger the optional phase input @ trigger.
Emitchange () {//monitor user input Lett = This; LetEditor = T.refs.editor; Letlen = editor.innerText.length; LetLastlen = T.totallen; T.totallen = Len;//deletion of text does not trigger the opening of the Select People dialog box if(Lastlen && len < Lastlen) {return} Letsel = Rangy.getselection (); LetRange = Sel.getrangeat (0);if(Range.commonAncestorContainer.nodeType = = = Node.text_node) {Range.setstart (Range.commonancestorcontainer,0); LetOriginstr = Range.tostring ();if(Originstr.substr (-1,1) ===' @ ') {//Reset range to include the @ characterRange.setstart (Range.commonancestorcontainer, Originstr.length-1);//Save bookmark rangy The non-standard API provided below is immediately described inT._bookmark = Range.getbookmark (Range.commonancestorcontainer); T.onmention ();//Open the Select dialog box} }}
Click button to trigger
The bookmark used in this case to complete the selection is set at Blur, if the initial situation is:
"123 [tag]"
Use ^ to represent the bookmark, which should be:
"123 [Tag]^"
But in fact, because the GetBookmark method in the rangy library used here is weaker than the imagined function, it only applies to the location where the text node is saved, and the result is as follows:
"123 ^[Label]"
Then the result of clicking the 3-times button to insert is:
"123 ^ [new label 3][new label 2][new label 1][tag]"
A simple method of processing
The Range.commonancestorcontainer after the @ tag is Textnode, and after the input tag is the editor node, which is Elementnode, Then in the completion of the selection of the part of the judgment, is the Elementnode are unified positioning to the end of the insertion.
Here is a practical way to move the cursor to the end of the element.
_setCaretToEnd(editor){ ifnull){ return } let selection = rangy.getSelection(); let range = rangy.createRange(); range.selectNodeContents(editor); range.collapse(false); range.select(); // 上一行的select是rangy提供的非标准api,可用以下两行代替 //selection.removeAllRanges(); //selection.addRange(range); return range}
Another way of customizing the settings for the bookmark is to describe it separately.
Completes the selection stage inserts the selected person's label at the specified position
Handlementionadd (persons) { Lett = This; LetEditor = This. Refs.editor.getDOMNode (); LetRange = Rangy.createrange ();//move to insertion position if(T._bookmark.containernode.nodetype = = = Node.text_node) {Range.movetobookmark (T._bookmark); }Else{range = This. _setcarettoend (editor); }//Create an HTML fragment that will be inserted LetMentionfragment = t._creatementionnode (persons); Range.deletecontents ();//Delete origin content in range like ' @ ' or nothing Save the end node before inserting the clip to reset the range later LetLastChild = Mentionfragment.lastchild; Range.insertnode (mentionfragment);//Reset range to the newly inserted label and move the cursor explicitlyRange.setstartafter (LastChild,0); Range.collapse (true); Range.Select (); T._bookmark = Range.getbookmark (Range.commonancestorcontainer); T.emitchange ();}
Returns the formatted text
Extractcontents (editor) {editor = Editor | | This. Refs.editor.getDOMNode (); Lett = This; Letnodes = Editor.childnodes; LetContent ="';if(Nodes.length = = =0){return "'}//Extract text, input people tags, and line breaks for( Leti =0, Len = nodes.length; i < Len; i + =1) { Letnode = nodes[i];if(Node.nodetype = = = Node.element_node) { LetTagName = Node.tagName.toLowerCase ();if(TagName = = =' input ') {//INPUT element Letitem =JSON. Parse (Node.getattribute (' Data ')); Content + = T.props.formatvalue (item); }Else if(TagName = = =' BR ') {content + =' \ n '; }Else{//Recursive call pasted HTML fragment may contain multi-layer nodesContent + = t.extractcontents (node)}}Else if(Node.nodetype = = = Node.text_node) {content + = Node.nodevalue; } }returnContent
Custom bookmark creation and positioning create a bookmark
The main idea is to set up a bookmark by adding two hidden marker points.
Serializable is true to save the node ID, mainly to be able to determine the existence of the bookmark based on the ID (if the user deleted the insertion point is not found).
CreateBookMark (range, serializable) {varStartnode, Endnode, BaseID, clone;varcollapsed = range.collapsed;varprefix =' __bookmark__ '; Startnode = Document.createelement (' span '); StartNode.style.display =' None ';if(serializable) {BaseID = prefix + ( This. _bookmarkid++); Startnode.setattribute (' id ', BaseID + (collapsed?' C ':' S ')); }if(!collapsed) {Endnode = Startnode.clonenode (true);if(serializable) {Endnode.setattribute (' id ', BaseID +' E '); }//Insert End nodeClone = Range.clonerange (); Clone.collapse (false); Clone.insertnode (Endnode); }//Insert Start nodeClone = Range.clonerange (); Clone.collapse (true); Clone.insertnode (Startnode);//Reset range position if(Endnode) {Range.setendbefore (Endnode); } range.setstartafter (Startnode);return{startnode:serializable? BaseID + (collapsed?)' C ':' S '): Startnode, endnode:serializable? BaseID +' E ': Endnode, Serializable:serializable, collapsed:collapsed}}
Navigate to Bookmark
moveToBookmark(bookmark, range){ range = range || document.createRange(); var serializable = bookmark.serializable, startNode = serializable ? document.getElementById(bookmark.startNode) : bookmark.startNode, endNode = serializable ? document.getElementById(bookmark.endNode) : bookmark.endNode; // 设置起止位置并移除标记点 range.setStartAfter(startNode); startNode.remove(); if (endNode) { range.setEndBefore(endNode); endNode.remove(); else { range.collapse(true); } return range}
Make a call
let selection = rangy.getSelection();let range = selection.getRangeAt(0);// 创建bookmarklettrue);// ... 完成选人之后需要进行插入标签// 移动到bookmark位置rangy.moveToBookmark(bookmark);
Some problems in the implementation process
- contenteditable element cannot focus
The body is set to-webkit-user-select:none causing the focus to be lost, so the element is reset to-webkit-user-select:text
- Click on the edit box on the phone (IPhone6 WebView and Safari below) focus is slow (requires deep click)
The PC's Safari does not have this problem, plus the OnClick event to trigger the focus event (judging from the non-focus state is triggered).
Another trigger focus will place the cursor at the beginning, where the Setcarettoend method mentioned above is called to move to the end of the line.
- Enter to change the line to get two BR tags, continue to input will eliminate the second BR
This is where the BR tag is processed when the input is formatted, and the extra line breaks are removed
content.replace(/\n$/, ‘‘).replace(/\n\n/g, ‘\n‘)
And when the formatted content is empty, do an empty process to clear the remaining BR
editor.innerHTML = ‘‘
- Get the Range object to block the subsequent execution of the Blur event in Blur event handling on the phone by selection
After debugging, it is found that safari selection the Rangecount of the object after the page blur (blur in Chrome retains the previous selection), so calling Getrangeat (0) will cause an error.
So one solution is to update the bookmark when the user enters (if the input is not the @ symbol).
Use of the mobile side
The core file size in the rangy library is 150k+, the introduction of the rangy library after compression packaging will bring about 47k volume increase, considering the mobile side of the browser compatibility is better than the PC side (Goodbye, IE), the above mentioned standard API can be called, Only some of the rangy API to replace the corresponding, mainly the following several:
// 1. rangy.getSelection 获取Selection对象let selection = window.getSelection();// 2. rangy.createRange 创建Range对象 也可用selection.getRangeAt(0)获取let range = document.createRange();// 3. range.select 用于显式地设置选区位置selection.removeAllRanges();selection.addRange(range);
As for the bookmark, we have already provided in the form of tool method above, so we can not judge the Commonancestorcontainer when we locate the bookmark, as long as we judge whether the tag node exists.
if(t._bookmark && document.getElementById(t._bookmark.startNode)){ range = rangy.moveToBookmark(t._bookmark, range);}else{ this._setCaretToEnd(editor);}
Summarize
First look at range feeling is relatively unpopular, think about, in the editor or there is a relatively rich application, whether rich text editor or online code editor, such as CKEditor, can be based on the range to achieve, but in the browser performance above, even if not consider IE, Some aspects still exist some details and imagined different, still need to do some processing, only and the line and attention.
Resources
https://dom.spec.whatwg.org/#introduction-to-dom-ranges
Https://developer.mozilla.org/en-US/docs/Web/API/range
Https://github.com/timdown/rangy/wiki/Rangy-Range
Dom range-A specific implementation of the @ feature