Today’s front-end domain: Serverless, low code, full stack and other concepts are everywhere. Developers talk about “how to make the front end bigger” and “how to land the concept at the top”. At this point, do you feel that “you have been abandoned by the future before you know where you are going?” Instead of wondering “what is serverless and how does it relate to the front end,” I thought we’d go back to the beginning of the requirements and start with the moat characteristics of the front end development. Stay true to the original aspiration and keep in mind the mission, front-end development is ultimately content rendering and interactive implementation. ** In today’s article, let’s start with an interesting product requirement and think about “where is the front end boundary?” from a different perspective. And from this front-end requirements, see how deep the technology can be practiced.
Understanding the requirements
The requirements are not too complicated, but simply implement “underline” and “insert notes” on a manuscript page. According to the following completion diagram, we can summarize the requirements including:
Public Note display:
- This is a document page, the main implementation of add underline and add notes two large functions
- Users can circle text and underline it in the pop-up Tooltip
- The wake Tooltip pops up when the user circles text or clicks on an already underlined block
- Tooltip displays the tooltip for selecting text and provides buttons such as Copy, Add/Remove Underline, Write note, and Share
- The above button function points are easy to understand and will not be expanded one by one
- Only document text supports underlined interaction, other page elements do not support underlined operation
- After the underlined text is added, add a highlighted background to the corresponding text
- Deleting an underscore deletes all notes corresponding to that underscore (if any).
- When the tooltip pops up, click the “Write Notes” button to navigate to the note editing page. After the user enters and adds the content, the manuscript page will be returned without refreshing, and the note will be inserted in the corresponding position. The note content needs to be inserted in the next line of the current line
- If there are other users’ public notes in a section of the page, the icon of public notes will be displayed after the section, and the number of public notes will be displayed in the icon
- Click the open note icon after the segment to display the open note content
More detailed requirements points and interaction details will be covered later in the implementation section.
Analyze requirements
Some readers may think: “underlined notes this kind of requirements I have seen, should not be difficult”, and even I have seen the article analysis of its implementation, such as: how to use JS to achieve “underlined words highlighted” online notes function? . In fact, unlike previous “underline” and “insert notes” requirements, our scenarios are really special, including but not limited to (please refer to the above completion diagram for understanding) :
- Legal lines can only be delineated as text inside the manuscript.That is, text in tooltip, notes added by the user, icon counts after a paragraph, blank lines, popover text, and so onAll non-original manuscripts are not supported
- ** user underline can be long or short, the underline range can be within a paragraph or across paragraphs. This difference affects the implementation of underlined highlighting and persistent data design
- The relationship between ** lines is complex, ** so different lines may appear: different line content cross, different line content full coverage (parent-child set relationship), different line content completely independent of three relationships
- Note insertion position corresponding to the underscore ** must be on the line following the underscore. If multiple notes are added to one underscore, these notes are sequentially superimposed on the line following the underscore
- The count of the post-segment public note count icon, which needs to change with the dynamic addition or deletion of lines and notes
- ** Tooltip positioning: The position of **tooltip needs to change with the content of the text selected by the user. It needs to always be in the center of the selected area, fixed pixel distance above the first line of the line vertically; If the checkbox fills a row, the tooltip needs to be fixed horizontally in the center of the screen
** All of these details need to be implemented on the React stack, ** because our documents are rendered via the React component:
return ( <div ... props> <Component1 /> <Component2 /> <RichText prop1={prop1} text={manuscript} prop3={prop3} /> <Component3 /> </div> )Copy the code
This presents a huge challenge: imagine React rendering a full page of rich text with setDangerouslyInnerHTML. How do we add a series of interactions, including underlining, to the rich text content? Or, more specifically, how do we find the line that the user underlined to add notes to? In this way, React may be a shackle that keeps us from doing what we can. Of course there is always a way, we continue to analyze and implement.
The core problem
Requirements involve many details, but the purpose of this article is not to cover them all. Let’s focus on “underline” and “add notes” first. There are three main directions for thinking about core issues:
- Adds a highlight style for underlined text
- Underline and insert notes
- Underline and persistent restoration of notes
It should be noted that the manuscript content of our manuscript page comes from background editor and third-party content import.
Editor so we see that the background for all the conventional content rich text editor, and contains a number of custom power, such as: add, add a code block, the reference formula style, image/video added, bookmarks, and automatic formatting (punctuation extrusion, the big three transformation, traditional simplified Chinese translation), etc. As a result, the content of a document is infinitely variable, and in theory any complex DOM structure can be present in the rich text of a document.
At the same time, highlighting and notes must support subsequent access to restore, a one-time one-time sale is meaningless.
So take the underlined highlighting implementation logic, which will appear in two scenarios:
- The first scenario is when the user enters the page and renders the page, the underlined lines and notes saved before are restored and displayed.
- The second scenario is that during the current page life cycle, the user adds underlines and notes dynamically.
Both scenes need to add highlighting backgrounds and so on, which, in code terms, requires logical abstraction and reuse.
In addition, during the requirements implementation process, we found that a core problem and risk point is event compatibility handling and conflict and interference resolution of event types. All of this will be covered later, so read on.
Industry information and community programmes
Before development, we investigated three implementations in the industry from the perspective of product form, which are:
- Netease Snail Reading
- Douban reading
- Medium
Among them, netease Snail reading is the closest to our needs, but similar needs only appear in netease Snail reading App, H5 end (WAP end) does not allow users to check text content.
Douban reading only the PC side to achieve the underlined requirements, the mobile side did not achieve underlined highlighting, and did not achieve the “underlined line after adding notes” function. It uses a separate page to display notes, which is tricky, but greatly easier to develop. These requirements are much cheaper to implement on the PC than on the mobile. It is worth mentioning that douban reading underlined style implementation scheme is to use a layer of absolute positioning mask, as shown below:
** But we abandoned the highlighting mask scheme for absolute positioning. * * our needs to realize a large number of mobile terminal interaction on the line, at the same time to achieve scribing lines after notes (notes content can not use absolute absolute positioning, because can’t open the line text content), then considering the different width of the phone’s screen, mask mask position should be dynamic computation, when crossed highlight magnitude is larger, It’s a computational cost that can’t be ignored. At the same time, it can be predicted that this scheme is not strong in the later expansibility and flexibility. Therefore, this “highlight style scheme with a mask” is not very suitable for our scenario.
Finally, Medium, which is no stranger to those of you who like to read technical articles from abroad. Medium’s small and fresh style experience is good, but in the underlined note function, the implementation is relatively simple. Again, it only has highlights and no notes (or notes are rendered as separate pages). In terms of technical implementation, as shown in the figure:
Medium adopts the scheme of removing and supplementing labels. It uses mark label to wrap underlined words, and sets the style of Mask label to achieve the effect of highlighting. ** But please note that this tag does not identify the underscore ID, which means that it cannot distinguish between different underscore ids. Because each underscore cannot be identified, when different underscores overlap, Medium will combine them to form a new underscore. When users delete or interact with other underscores, they will operate the new underscore generated by the combination. ** is of course the simplest implementation, but our product is not satisfactory.
Therefore, the products in the industry are relatively basic compared with our needs. They are:
- Lines/overlaps are not supported at all
- Only App implementation, or only PC implementation, mobile terminal H5 page is not implemented
Related open source libraries
Take a look at community-related open source libraries:
- Rangy can implement text highlighting, but in the case of underlined overlap, it directly merges the two selections, which of course is not suitable for our business requirements
- Diigo, which not only requires a fee, but is extremely weak, also does not directly allow the overlap of underlined selections
- The customization capability of Web-highlighter is weak
Open source libraries are available for each solution, and I won’t break them down here. In conclusion, the community wheel is more like a toy, and even though it supports overlapping underlined areas, it is more of a style-highlighting demo, which is more of a challenge if we want to interact with it in the future, such as inserting notes after a line, clicking underlined to wake up the tooltip, etc.
Combining our specific needs, flexibility and autonomy, we decided to roll up our sleeves and do it ourselves.
The development train of thought
The complexity of the requirements meant that our implementation was different from community and industry solutions, or an upgrade or modification of existing solutions. In addition to reflecting the traditional knowledge of the front-end, the whole idea of implementation highlights the application of algorithm and even compiler principle.
Underline style implementation
First focus on the underlined style approach. In simple terms, we do this by disassembling the label. As shown in figure:
The first line indicates the content of the existing document snippet, and the orange font 3456 indicates the user-underlined area. Our expected result is to wrap 3456 in a new span tag that contains the id of the crossed line and other information.
In the case of multiple crossed lines, see the following schematic diagram. There is already crossed line 3456. If the user selects crossed line 5678, 56 belongs to the crossed area of two crossed lines. Ideally, we should get the new tag structure:
** In this way, the design of disassembly labels is more flexible than the absolute positioning mask of douban reading analyzed above: ** Douban reading scheme is undoubtedly only the display of style, if the event interaction is considered, then the stability and strength of disassembly labels is obvious. For example, when the user clicks 56 text, the tooltip appears. If the “Delete underlined” button is clicked, as required, we should remove the most recent underlined line (5678) instead of 3456, which disintegrates labels to generate different tag details that work well with front-end event handling.
** We bubble by mimicking natural events: ** If the DOM element is iterated up according to event.target, it can be found that 56 not only belongs to the underscore whose id is 2, but also belongs to the underscore whose ID is 1. By comparing the creation time of the underscore, it can find the final underscore that needs to be performed (delete, share, add notes, copy content, etc.).
This logic is abstracted as a function, which is simply expressed as:
GetLatestHighlight (e) {// Select the text to check for repetitions, Set the targetHightlightId if (this. State. TargetHightlightId &&! E) {return this. State. TargetHightlightId} let the target / / triggerTooltipEvent used to trigger the compatibility of events and circumstances of smooth, If (triggerTooltipEvent &&! E) {target = triggerTooltipEvent. Target} else if (e) {target = e. arget} else {/ / unable to get the target object is impossible (in theory), Return} const propagationHighlightMap = {} const paragraphNode = getFirstBlockAncestor(target) let latestHighlight = target. The getAttribute (' createdtime ') / / triggerTooltipEvent target bubbling up, Throw all crossed (possibly overlapping and crossing) information into the propagationHighlightMap const walk = node => {while (node! == paragraphNode) { if (node.getAttribute('commentid')) { const currentHighlightCreatedtime = node.getAttribute('createdtime') if (Number(currentHighlightCreatedtime) > Number(latestHighlight)) { latestHighlight = currentHighlightCreatedtime } propagationHighlightMap[currentHighlightCreatedtime] = node.getAttribute('commentid') } node = node.parentNode } } walk(target) const latestHighlightId = propagationHighlightMap[latestHighlight] return latestHighlightId }Copy the code
Based on the underlined rich text tag result, we can call the React setDangerouslyInnerHTML API to get the underlined page.
“Paper come zhongjue shallow, and must know this to practice.” As you implement your solution, you’ll find that “it’s easier said than done.” For example, the “disassembly label” has been mentioned, how to disassemble, how to solve it? There are two label disassembly scenarios in front of us, which I categorize as:
- Dom based
- String based
The first is based on the DOM and the second is based on strings. Before going into detail, let’s familiarize ourselves with two basic BOM/DOM APIs.
Window.getSelection
Window.getselection () returns a list of information about the user’s selection, used as follows:
const range = window.getSelection().getRangeAt(0)
const start = {
node: range.startContainer,
offset: range.startOffset
}
const end = {
node: range.endContainer,
offset: range.endOffset
}
Copy the code
Note, however, that it does not directly retrieve all of the DOM elements in the selection. It only returns information about the beginning and end nodes of the selection, including the node where the underscore began and the offset of the starting text relative to that node. The node where the underscore ends and the offset of the closing text with respect to that node. ** We found the beginning and end nodes exactly, and the next step is to find all the text nodes “in between”. This requires traversing the DOM tree.
The typical tree DOM Node is shown here:
** Since the DOM is not a linear structure but a tree structure, “find all the text nodes in the middle”, which is translated into programming language, means depth-first traversal. ** Let’s look at the code:
<p>12
<span data-id=1> 34
<span data-id=2> 56 </span>
</span>
<span data-id=2> 78 </span>
90
</p>
Copy the code
This text contains two underlined paragraphs. Corresponding graph:
If the user checks 67, then we want to get:
<p>12
<span data-id=1> 34
<span data-id=2>
5
<span data-id=3> 6 </span>
</span>
</span>
<span data-id=2>
<span data-id=3> 7 </span>
8
</span>
90
</p>
Copy the code
Since the next sibling of startNode (span with data-ID 3) we can get is null, we need to “backtrack” and go further down in order to traverse the package tag and find endNode. Typical DFS, which can be implemented with both loops and recursion, will not be described here, but a simple illustration is given:
The recursive version:
const DFSTraverse = (rootNodes, rootLayer) => {
const roots = Array.from(rootNodes)
while (roots.length) {
const root = roots.shift()
printInfo(root, rootLayer)
if (root.children.length) {
DFSTraverse(root.children, rootLayer + 1)
}
}
}
Copy the code
Stack pseudocode:
stack my_stack;
list visited_nodes;
my_stack.push(starting_node);
while my_stack.length > 0
current_node = my_stack.pop();
if current_node == null
continue;
if current_node in visited_nodes
continue;
visited_nodes.add(current_node);
// visit node, get the class or whatever you need
foreach child in current_node.children
my_stack.push(child);
Copy the code
Text.splitText()
Since the user-checked area contains only part of a text node, we unpack the label at part of the start and end nodes as well. Most readers will probably think of text.splittext () to splitText nodes. With text.splitText (), for the start node, collect the second half of it; For the end node, the first half is collected.
As shown in figure,
code
if (curNode === $startNode) {
if (curNode.nodeType === 3) {
curNode.splitText(startOffset)
const node = curNode.nextSibling
selectedNodes.push(node)
}
}
if (curNode === $endNode) {
if (curNode.nodeType === 3) {
const node = curNode
node.splitText(endOffset)
selectedNodes.push(node)
}
}
Copy the code
DOM -based scheme
With the above foundation, we can easily implement a DOM based scheme to disintegrate labels and achieve underlined rendering.
How can we use JS to achieve “underline words” online notes function? The scheme provided in this article first calculates the offset from the beginning to the end of the marking label:
function getTextPreOffset(root, text) { const nodeStack = [root]; let curNode = null; let offset = 0; while (curNode = nodeStack.pop()) { const children = curNode.childNodes; for (let i = children.length - 1; i >= 0; i--) { nodeStack.push(children[i]); } if (curNode.nodeType === 3 && curNode ! == text) { offset += curNode.textContent.length; } else if (curNode.nodeType === 3) { break; } } return offset; }Copy the code
To restore a highlight selection, a corresponding inverse procedure is required:
function getTextChildByOffset(parent, offset) { const nodeStack = [parent]; let curNode = null; let curOffset = 0; let startOffset = 0; while (curNode = nodeStack.pop()) { const children = curNode.childNodes; for (let i = children.length - 1; i >= 0; i--) { nodeStack.push(children[i]); } if (curNode.nodeType === 3) { startOffset = offset - curOffset; curOffset += curNode.textContent.length; if (curOffset >= offset) { break; } } } if (! curNode) { curNode = parent; } return {node: curNode, offset: startOffset}; }Copy the code
The ideal was rich, but I ended up abandoning the DOM based solution. Here’s why:
- DOM Based solutions rely on DOM Nodes, which can’t be implemented directly unless your content is rendered to the browser (or through some hosting API)
- If rendered to the browser, each step of rendering depends on the updated DOM of the previous step rendered to the browser, reading and writing the DOM repeatedly, meaning repeated repaint or even reflow. Each underlined rendering also manipulates the DOM frequently. Our React based application repeatedly sets state to trigger updates to the setDangerouslyInnerHTML content, and the experience is devastating
- DOM dependence means that there are a lot of weird issues with compatibility, which means “mysterious powers of unpredictability.”
The biggest disadvantage of dom-based tag disassembly schemes is that we are completely dependent on the DOM tree, so a string-based tag disassembly scheme seems more appropriate for both stability and flexibility.
Underline persistence and restore
In order of this article, we should introduce the string based tag disassembly scheme. But in order to get a clearer picture of how this works, let me leave this scenario aside for a moment, and let’s take a look at the highlighted persistence and restore practices.
The key to persistent underlined highlighting selection is to find an appropriate way to serialize the DOM nodes so that when you re-enter the page you can locate the DOM nodes and render underlined and highlighted content.
There are four general schemes:
- XPath: An xPath that records the underlined DOM
- Css Selector: Records the order of label selectors in the underlined DOM
- Dom tag node offset + text offset: records the offset of the underlined Dom tag and the offset of the underlined text in the Dom
- Paragraph offset + text offset: Records the offset of the Paragraph to which the underlined DOM belongs and the offset of the underlined text with respect to the Paragraph
Our first response is to record the offset of the relevant DOM, that is, “which DOM is underlined”, and then record the relative or absolute offset of the DOM. This is the idea of the first three schemes. In fact, I initially chose to use the third method for quick implementation, but there were some “fatal problems”. The third way is to record the offset of the DOM tag, that is, the underscore relative to the KTH tag of all rich text content, and the offset of the text within the tag relative to the DOM text (from the beginning or end of the K’ character of the tag).
Or how to use JS to achieve “underline words” online notes function? As an example, let’s look at the problem with this persistence scheme.
As follows:
<p> I am very happy to be here today to share with you the implementation of text highlighting (online notes). </p>Copy the code
The user underlined the word “happy” :
<p> Very <span class="highlight"> happy </span> to be here today to share with you how text highlighting is implemented. </p>Copy the code
Let’s generate the relevant underlined data:
Start: {tagName: 'p', index: 0, childIndex: 0, offset: 2}, end: {tagName: 'p', index: 0, childIndex: 0, offset: 4 } }Copy the code
This is not hard to understand. The word “happy” appears in the first P tag, and there is only one text node in the P tag, so the childIndex is 0. The second word of the text node is underlined and the fourth word is terminated.
At this point, the user underlined the words “text highlight” :
<p> very <span class="highlight"> happy </span> to be here today to share with you how < SPAN class="highlight"> text highlights </span> (online notes) are implemented. </p>Copy the code
At this point, the calculation of persistent data is based on the DOM snapshot generated at the last moment, that is, the underline of the words “text highlight” is calculated relative to the DOM structure at the last moment: The childIndex of the first and last nodes is denoted as 2 (P tag has three children), and the offset of the words “text highlighting” is calculated relative to “I’m here today to share with you how text highlighting is implemented”.
Get a new data structure:
// The serialized information obtained when the words "text highlight" are highlighted. // At this point, there is already a highlighted message (" happy ") under p. // So its internal HTML structure has been modified. Intuitively, childNodes has changed. // Furthermore, the childIndex attribute is changed to 2 due to the addition of the previous span element. { start: { tagName: 'p', index: 0, childIndex: 2, offset: 14 }, end: { tagName: 'p', index: 0, childIndex: 2, offset: 18}}Copy the code
Imagine that if the user removes the underscore for the Happy selection again, all underscore data that appears after the underscore for the happy selection will be in error. ** Essentially, with the underline addition, we dynamically change the DOM structure, causing the persistent data to get messed up, and that’s where the problem lies. Note paragraphs can also be added dynamically to requirements at any time, which undoubtedly complicates the problem.
Therefore, a reasonable underlined persistence and restoration scheme should record the offset of the document text relative to the text, not the DOM tag compiled.
Let’s take a look at the final plan:
The corresponding data is:
Notes field represents note information. Note requirements are not involved here, so we can ignore them for now.
Field description: Paragraph_start and paragraph_end indicate the start and end paragraphs of the current line. If the corresponding values are not equal, it indicates that the line goes across paragraphs. Mark_start and mark_end represent the offsets of the beginning and end paragraph_start and paragraph_end, respectively, relative to the plain text of the paragraph.
String -based scheme
Now that we know about persistent highlighting and highlighting schemes, let’s strike while the iron is hot and see how string based schemes combine persistent data to implement highlighting (untag).
The first thing to note is that we can’t just arbitrarily line the start and end of a span and add the start and close tags, because that would be too ideal and would result in mismatched tags.
Such as:
< P > Today < SPAN > I am very happy to </span> introduce </ P >Copy the code
I’m very happy. At this point, the user underlined the seven words “very happy to give you”, if directly added the label, will get:
< p > < span > < span > I was very happy today < / span > to < / span > introduction < / p >Copy the code
Obviously span hierarchy is out of order and package labels are invalid.
When we do underline, we get the basic information rich text is a string, such as this:
<p>123456<span>789012345</span>678901234567</p>
Copy the code
Assume that the actual content to be highlighted is as follows, with the underline starting at the first 9 and ending at the second 9:
In other words, I want to get the above effect.
Combining underscore data: mark_start, mark_end, we first need to find the character where the underscore begins. Set the pointer and start scanning the string one by one:
Scanning until the pointer is offset to mark_start indicates that the underscore start is found. We insert the SPAN string, and finally iterate through the line to get the fully decorated rich text string, which is rendered in the React setDangerouslyInnerHTML in one go.
But here’s a caveat: Since mark_start and mark_end are offsets from the text, if we encounter a DOM tag and enter the text inside the DOM tag while scanning the tag, we need to stop counting and continue moving the pointer until the current DOM tag is removed before resuming the count. That is, in the figure above, we do not count the six displacements that we scan up to and beyond.
If you’re wondering, wouldn’t it be easier to keep track of the offset relative to the rich text content? Congratulations, if you think of it that way, we’re back to the problem of dynamically changing DOM tags that are rich text content.
In fact, the DOM tag’s opening tag character < and closing tag > are both escaped. But I want to extend here, even without escaping, the real question is: How do we tell if we’ve encountered a DOM tag during pointer movement and need to stop counting? Since the content of the document might have a <, how do we know that this is the actual content of the document and not the tag that goes into the DOM tag? (Actually < will be escaped, but we’re asking a simplification question here, leaving the escape case out of the question).
** This process is already a prototype of a modern compiler, and we can see how the compiler handles this problem: ** When < is scanned, we set the second pointer, which continues to sniff down and scan. If <span is matched all the way we can assert that the current < encountered by the first pointer is a DOM tag start tag.
In fact, those familiar with Vue’s source code might think of the Vue Compiler module: the same problem is addressed when Vue implements the template engine and binds template variables in both directions. Because this is a classic compiler rationale.
** When compiling code, such as Babel, ** such as optional chaining, also scans the source string to find a compiler. , scan down through the new sniffer pointer. If? Followed by a., which is foo? Bar is an optional chaining expression, and we can compile it in ES5. If the sniffer pointer finds? Then it should be understood as a ternary operator. The real situation is more complicated and somewhat different, and here is just to explain the principle, not to expand.
The technical term for this pointer sniffing implementation, which you may have heard of, is tokenizer, or participle. It often appears in combination with AST (Abstract syntax tree), and appears in fields such as front-end engineering.
The label disassembly of underlined notes is a simple compiler principle, involving a series of processes such as Tokenizer. With this ability, everything becomes “less complicated.” Of course, in the implementation of underlined highlighting business, I created a lot of FastPath according to the requirements of the characteristics, simplify the process of compiling word segmentation, here readers only need to understand the underlying ideas.
Insert notes after lines
After highlighting the underline, let’s look at the implementation of the insert note effect after the underline line. As mentioned earlier, all of the text on the React page was rendered with rich text. After the underlined text, how to find the appropriate place to insert notes?
The ** scheme is very clever: ** Using the document.createrange () and text.splitNode () APIs, create a range of length 1 after the first character of the line, in other words, Extract every word after the last word of the underlined line and calculate the length of the word from the left side of the screen, and record. As a rule, each underlined word should increase in length from the left side of the screen until the first character after the newline. So we find the beginning of the line after the underscore.
As shown below:
The next step is to insert the logic of the note node where the newline is found. Simple to say, in addition to the implementation of recursive algorithm, but also use a variety of cases of fault tolerance processing, the implementation code also has hundreds of lines.
Code:
const walkToRenderCommentBlock = (range, lastRightOffset, currentAnnotationId, lastAnnotationByIdNode) => { const currentRightOffset = range.getBoundingClientRect().right if (lastRightOffset > CurrentRightOffset) {// Insert point doRenderCommentBlock(range, currentAnnotationId, LastAnnotationByIdNode)} else {/ / continue down a character to find the if (range. EndOffset < range. EndContainer. TextContent. Length - 1) {/ / If the current range hasn't found the header, Const currentRange = document.createrange () currentRange.setStart(range.endContainer, Range.endoffset) // If the end Node type is one of Text, Comment, or CDATASection, then endOffset refers to the offset from the end Node. EndOffset refers to the offset calculated from the end node to the child node. Try {// Range. EndOffset + 1 does not exist (such as empty label), Currentrange.setend (range.endContainer, range.endOffset + 1) } catch (e) { currentRange.setStart(getNextSiblingNode(range.endContainer), 0) currentRange.setEnd(getNextSiblingNode(range.endContainer), 1) } return walkToRenderCommentBlock(currentRange, CurrentRightOffset, currentAnnotationId, lastAnnotationByIdNode)} else {// If the current range is up and not found, NodeText const nextNode = getNextTextNode(range.endContainer) if (nextNode) {const currentRange = document.createRange() currentRange.setStart(nextNode, 0) currentRange.setEnd(nextNode, 1) return walkToRenderCommentBlock(currentRange, currentRightOffset, currentAnnotationId, LastAnnotationByIdNode)} else {// Find the end of the preceding paragraph, Add doRenderCommentBlock(NULL, currentAnnotationId, lastAnnotationByIdNode)}}}} to the end of the segmentCopy the code
Overall process combing
Let’s take a look at the sequence diagram of the whole process:
Rect-based SSR architecture is adopted in the project, and two types of data are pre-acquired on the server side:
- fetchManuscript
- fetchAnnotationsData
The first type of data is the original manuscript; The second type is the underlined note persistent data corresponding to the document. After rendering it to the browser, React does its first rendering, which renders the original text. The logic of scribing notes can only be applied after the actual rendering of the original document is completed, combined with the screen width and location of the phone. In componentDidMount logic, we first make disableSelection, which disables all non-document content, and then the transformData logic processes the back-end data as:
- annotationsById
- notesById
The underlined highlighting logic core function: renderHighlight iterates over annotationsById, generating a full string of rich text with underlined tags, at which point rendering is triggered again. After rendering this time, renderNotes and renderNotesIcon functions are executed, which iterate over the notesById to generate a full string of rich text that has been added to the note block, and trigger rendering.
The whole process is executed sequentially through promises. Note that this cannot be done in parallel because the insertion of notes depends on the layout after rendering the underlined style.
Because our event handling and binding uses the event broker approach, it can be processed in parallel with other rendering processes after componentDidMount. As we mentioned at the beginning, the conflict and interference of events are particularly prominent and troublesome in this project. Due to space constraints, we will not go into depth analysis, but to break down a few items for your reference.
Touch event Handling
You may wonder why you need to listen for touch events. Is it the 300 ms delay for Click on mobile?
Window.getselection () is not that simple, because when the user clicks on the text, the checkbox will be empty for system reasons, and window.getSelection() will be null. Window.getselection () is usually required for click events on tooltip. Therefore, we can either persist the last window.getSelection() return value in memory, or replace the tooltip click event with a touch event, which is obviously a more reasonable solution.
Next, let’s look at what interactions are bound to touch events (specifically, touchend events).
copy
There are two scenarios for clicking the copy button:
- While the user is checking the text, click “Copy” on the Tooltip to copy the legal text.
- One way is to click on an existing underline, wake up the ToolTip, and then click the Copy button on the ToolTip to copy the underlined text.
Simple to say, complicated to do. For the first case, we need to find the legal check text, the requirements of check content if contains the notes, then copy copy to exclude the notes content, only copy the manuscript content; If a section is included and the note count icon is exposed, it cannot be copied into the count. Therefore, we also need to traverse the selected area and identify illegal tags (non-text tags). Select startNode and endNode, and then start DFS.
In the second case, after clicking the “copy” button, we need to determine whether the click area belongs to the crossed area highlighted by multiple lines. If so, we need to simulate the upward bubbling process to find the nearest home line and copy the corresponding content.
- line
By clicking the underline button, we determine whether the selection can be underlined, calculate the offset of the underline to get the underlined persistent data, untag it, render the highlighted area, and then send requests to the back end and update annotaionsById data in memory. Don’t forget to count the icon after updating the segment.
- Delete the line
Similar to the underlined button, you also need to determine whether the click area is a crossed area with multiple underlined lines, communicate with the back end, and modify memory data and DOM content.
- share
To share, you need to adjust the client’s capabilities. Again, you need to decide whether to click the underlined button to wake up the Tooltip and click the “Share” button, or whether to click the text box to wake up the Tooltip and click the “Share” button. The basic logic is similar to the “copy” button click.
- Write a note
Perhaps the user has checked the new content: underline it before taking notes; Or click on an existing underline to add notes.
Thus, various button logic has multiple trigger scenarios, requiring a lot of careful judgment and processing. This is just the “tip of the iceberg”, and more logic and scenarios are no longer enumerated.
Romance of The Three Kingdoms of Click, Touch, SelectionChange
The introduction of touch events refines the granularity of our event processing to enable the requirements to be fulfilled. But it brings a confluence of events and conflicts. Combined with the fragmented mobile terminals, conflicts abound.
For example, for click, if the current toolTip does not exist and the underlined content is clicked, it should wake up toolTip with a “remove underlined” button. If the current tooltip already exists, a blank area is considered to have been touched and the tooltip should disappear. In some browsers, the selectionChange event is triggered by click, but when the selectionChange event is triggered, it makes us think that the user has checked the new content, causing a chain reaction. Again, when the tooltip exists, click on the blank area and the tooltip disappears. However, the requirement was that the tooltip should not disappear while scrolling, and when scrolling is triggered, many browsers also trigger click events, so we decided that the tooltip should disappear again. All of these things are intertwined, and developers need to take them into account.
Serious readers may be wondering “Why not listen for the selectionChangeend event”. During development, we discovered that the selectionChangeend event was not triggered on any phone, although it was mentioned in the specification. If touchEnd is used to simulate selectionChangeEnd, it is found that some mobile phones do not trigger TouchEnd/TouchMove after checking the end.
Of course, that’s not the point of this article, so we’ll leave it at that.
Security and performance assurance
Overall, the safety of scribing notes is particularly important. Security here mainly refers to the non-obstruction of user interaction and the accuracy of presentation of manuscript content and underlined notes. However, the document page content is constantly changing, and the tag structure can theoretically be the most complex. How do you keep the page from clogging up and the rest of your interactions running smoothly when something goes wrong online? Try… Catch block, pay attention to error collection and recovery in catch, convenient for subsequent recording and tracing. At the same time, a reasonable fallback mechanism is also very important, which needs to discuss with the product to make a more perfect plan. It is worth mentioning that our front end team is now making efforts to build a perfect end-to-end testing process, which has been connected with the most basic underlined testing. In the future, we will continue to deepen our efforts in end-to-end testing, and we will share more experiences and insights.
The performance topic is actually more common, and the security strategy is also more common, but the performance means behind each point is a great topic, here we simply summarize the performance optimization methods used, and no longer extend down:
- Server render, prefetch data
- Optimization of Dom node selectors
- Recursive performance optimization (preferred for loop, optimized with tail recursive calls such as trampoline functions)
- Proper use of debouch and throttle
- Dom manipulation reduces repaint and reflow
- Independent composition layer, GPU rendering speeds up correlation: use of CSS3 properties such as transform, opacity, etc
- AddEventListenner Use of the third parameter passive
conclusion
From the requirements of “underline” and “insert notes”, we extracted a series of front-end knowledge points, and analyzed the implementation difficulties and solutions.
These contents cover the basic knowledge of DOM, BOM, AST and compilation principles, which are indispensable in the programming field, and extend the implementation principle of Babel and Vue, which modern front-end development relies on.
One of the moats of ** front-end development is the elaboration of interactive implementation, front-end development of the frontier expansion soil also depends on the lower level of programming universal principle, ** hope this long article can inspire you, but also welcome to discuss.