Twinkling of an eye, today is 29. And I’ve been playing Kings at home for two days.

Why write this article? Er… It starts with two pictures.

Dozen king dozen oneself dozen since closed 😭, as a result run to dig gold plan to see who is actually still at work today (hey hey).

Then I found that there was an essay contest, so I came to lower the HXD’s winning rate. 😄 😄 😄

The text start

In this article we’ll encapsulate a react underline component. Underline function is quite common around us. Such as wechat reading

Or some Chrome extensions like those of us who are good at learning (bah)

It simply selects text and highlights it.

But that’s all it can do. Want to achieve it is also a little difficult, next little white bit by bit with you to appreciate

1. Demand analysis and search for ideas

Requirements analysis is not needed here, the functionality is as simple as it can be. So let’s think about how do we do that

There are three main functions of traditional word marking

  1. Traverse the DOM tree to collect nodes that require Mark
  2. Dom serialization and deserialization
  3. Dealing with duplication

Dealing with repetition isn’t difficult. It just depends on what strategy you choose, such as whether you’re covering things up or not allowing repetition at all. It’s entirely up to you. Therefore, I will only elaborate on the first two in this article

Analysis (we first say the idea, then say concrete implementation)

1.1 Traverse the DOM tree to collect nodes that require Mark

As shown above, (p stands for a P tag) we have selected I am text I am, and for it to be highlighted we have to get this text node. How to take?

In fact, I used an API to get the cursor in the last article. Oh, I forgot that it’s so popular anyway

Let’s just say there’s an API in the front end to get some information out of the selection. Such as which node is selected, start or end, offset, etc. (check the documentation if you are interested)

I don’t think it’s necessary to explain its detailed use separately in this article, so I can tell you. We have a way to get the start and end nodes of the selection and the text offsets in them

This may not be intuitive, for example (let’s select the first text in the P node)

You can see that its starting node is a text node with an offset of 0. The end node is also a text node with an offset of 6

Take a closer look

   <p>I am the most happy New Year, do not worry about being late, do not have to do homework, sleep to wake up naturally every day, even if occasionally naughty, adults will laugh it off, will not blame.<span>Level 2<span>Level 3</span></span>
Copy the code

The current P tag has two children, the first being a text node and the remaining being a container node.

Take another look at the text offset. It’s true that there’s nothing in front of it in the beginning and it starts at zero. What’s the end? It is true that I am most happy with the offset of 6 stations

But what we want to get is the New Year I’m most happy, this text node. Is there a way to truncate a long text node?

There is an answer, of course

The node.splitText () method is exactly what we need

Now we can get the New Year I am the most happy text node. How do you do that?

This is easy to do, enclosing a container node outside the text node. Just add a little style to the container node

Like this,

Gee, that’s easy. What’s wrong with that?

Oh, come on… You think this is the end of the process. Sorry, man. We’re just getting started here

In reality, though, the text looks like this side by side. But they do.

Normally, we need to deal with this (such as text from the first text node to the last text node).

Ha ha, in fact, this time as long as your hands away from the keyboard, pick up the tea has been brewing next to a gently sip. Would look at the screen contemptuously and say, “That’s it?”

Haha, just kidding. And it’s really easy to deal with. This is obviously a graph structure (although I don’t draw it on my graph, remember that the siblings of dom nodes are also related to each other). DFS or BFS is not an arbitrary choice. We know where we start and where we end. Just look at the boundary problem, you don’t want to go to the top.

That concludes our analysis of the first problem.

1.2 DOM serialization and deserialization

Above we have the idea of how to select a text and make it highlighted, but it is only one-time, what does that mean? That is, the above things can not be persisted, the page can not be refreshed. So we have to record it on the server or locally

How do I record it?

There are two common data formats that can be passed to the background or stored locally

  1. json
  2. string

Not strings, the only option is json strings. Therefore, our next main task is to convert Mark’s text nodes into corresponding JSON information to achieve positioning records (that is, to serialize).

We slowly implement from simple to difficult. First, we can locate the parent node of the Mark text node

To understand this, assume that the MARK text node is “text”. We can record the parent node first, and then during the deserialization process, we can use JSON information to locate the parent node, and then get its child node

How do I record this parent?

It’s easy. Suppose the container node in the figure above looks like this

So we can record the parent node of the Mark text like this

  1. Get the parent node’s label “SPAN” and collect all span nodes in the current tree

    Like this (of course, if you use a span for your container, then it is best to remove mark’s SPAN nodes when collecting span nodes)

    const tagName = node.tagName
    const list = root.getElementsByTagName(tagName)
    Copy the code

    Now lis is an array-like container that records all span nodes of the DOM tree, so do all span nodes in it have subscripts? We use the subscripts to locate target container nodes.

    For example, the image above (a simple span node)

    The list container looks like this

So we can locate the SPAN node with just an index. Maybe now you are a little curious how to deserialize location

Hahaha, it’s easy. In deserialization you do the reverse

 const parent = root.getElementsByTagName(tagName)[index]
Copy the code

Now we’re going to go down the difficulty level, and most of the time we’re not going to be dealing with a selection like this. Too simple

Most of it is

It is not enough to locate the parent container node, the goal is to locate the “text ah” text node

There are two ways I’ve done this. The first is at the dimension of the node (which is problematic because the DOM tree is constantly changing and I left it out) and the second is at the dimension of the text that I ended up using

What does that mean?

Because we are highlighting the span around the text node, this causes the DOM to be in constant flux. Therefore, in the node dimension, the relative position between nodes is no longer trusted.

So what is constant?

A quick look at the figure above shows that the positions between texts never change. That’s all right

We can “flatten” the text by taking the span parent node and then DFS taking all the text nodes within it. It looks like this after collection

Remember that the execution dimension of serialization is on the Mark text node, which means that there are two Mark nodes for serialization in the current example. They are “text” and “ah”

So how do I locate the text

Suppose the above container to collect text nodes is called allTextNode and the mark textnode is textnode

So let’s just look at the left and right offsets of TextNode in allTextNode

let Index = allTextNode.findIndex(textnode => textnode === textNode)

In the end what I want is to get the text to an exact location on all the text nodes of its parent node, which may be unclear

For example, the mark node “text” is what I want to end up with

ChildIndexStart: 2

ChildIndexend: 4

That is, we can use these two offsets to locate the text exactly. It starts at 2 and ends at 4

Okay, so now we have basically all the information we need

The final form of node information after serialization

childIndexStart: 2
childIndexend: 4
index: 0
tagName: "SPAN"
Copy the code

Well done, the idea is that there is a concrete implementation

Two: concrete implementation

Ideas are in the above, here I mainly put my demo code post. Make up the words…

2.1 Traverse the DOM tree to collect nodes requiring Mark

Gets the start and end nodes and their offsets

 const electoral = () = > {
        markArr = []
        flag = 0
        let range = getDomRange()
        if (range) {
            // Get the start and end positions
            const start = {
                node: range.startContainer,
                offset: range.startOffset
            }
            const end = {
                node: range.endContainer,
                offset: range.endOffset
            }
            console.dir(start)
            console.dir(end)
            let newNode
            // 2. Handle the case where the beginning and end of ----- are a node, and take an intersection
            if (start.node === end.node) {
                newNode = splitNode(start.node, start.offset, end.offset)
                data.push(serialize(newNode))
                parseToDOM(newNode)
            } else {
                // Multi-node case
                traversalDom(start, end)
                markArr[0] = splitHeader(start)
                markArr[markArr.length - 1] = splitTail(end)
                let RDArr = [...new Set(markArr)]
                // Serialization processing
                RDArr.forEach(node= > data.push(serialize(node)))
                // Package handling
                RDArr.forEach(node= > {
                    parseToDOM(node)
                })
            }
            localStorage.setItem('markDom'.JSON.stringify(data))
        }
    }
Copy the code
export const getDomRange = () = > {
    const selection = window.getSelection();

    if (selection.isCollapsed) {
        return null;
    }

    return selection.getRangeAt(0);
};
Copy the code

There are definitely two situations when dealing with nodes

  1. The start node and the end node are the same text node, which is simpler
    const splitNode = (node, header, tail) = > {
        let newNode = node.splitText(header)
        newNode.splitText(tail - header)
        return newNode
    }

Copy the code
  1. It’s not the same node. It’s a little bit too much. (I’ve made it too complicated here, but I can simplify it. The code is a bit messy, but that’s all it is.)

    1. First we need to traverse the DOM tree to collect all the text nodes between the start node and the end node
    2. The first and last nodes are treated separately
       / * * * *@param {*} start 
         * @param {*} End * DOM tree traversal */
        const traversalDom = (start, end) = > {
            let currentNode = start.node
            if (currentNode.nextSibling) {
                while(currentNode ! = end.node && currentNode.nextSibling ! =null) {
                    collectTextNode(currentNode, end.node)
                    currentNode = currentNode.nextSibling
                }
                if (flag == 0) {
                    collectTextNode(currentNode, end.node)
                    findUncle(currentNode, end.node)
                } else {
                    return}}else {
                collectTextNode(currentNode, end.node)
                findUncle(currentNode, end.node)
            }
        }
    Copy the code
        / * * * *@param {*} node 
         * @param {*} EndNode * find uncle 😀 */
        const findUncle = (node, endNode) = > {
            if (node == markRef.current) {
                return
            }
            let currentNode = node
            let current_fa = findFatherNode(currentNode)
            // See if it is the last akron bomb of the current node. • • • * ~ *
            if (current_fa.nextSibling) {
                collectTextNode(current_fa.nextSibling, endNode)
                if (flag == 1) {
                    return
                } else {
                    currentNode = current_fa.nextSibling
                    while(currentNode.nextSibling ! =null && flag === 0) {
                        collectTextNode(currentNode.nextSibling, endNode)
                        currentNode = currentNode.nextSibling
                    }
                    if (flag == 0) {
                        collectTextNode(currentNode, endNode)
                        findUncle(currentNode, endNode)
                    } else {
                        return}}}else {
                collectTextNode(currentNode, endNode)
                findUncle(current_fa, endNode)
            }
        }
    Copy the code
        / * * * *@param {*} node 
         * @param {*} EndNode DFS collection */
        const collectTextNode = (node, endNode) = > {
            // dfs
            if (node.nodeType === 3) {
                pushTextNode(node)
            } else {
                let childNodes = node.childNodes
                if (childNodes) {
                    for (let i = 0; i < childNodes.length; i++) {
                        if (childNodes[i].nodeType === 3) {
                            pushTextNode(childNodes[i])
                            if (childNodes[i] == endNode) {
                                flag = 1
                                return}}else {
                            collectTextNode(childNodes[i], endNode)
                        }
                    }
                } else {
                    return}}}Copy the code
    
        / * * * *@param {*} node* Mark collects */
        const pushTextNode = (node) = > {
            if (markArr.findIndex(item= > node === item) === -1) {
                markArr.push(node)
            }
        }
    Copy the code
  2. Span is wrapped around the Mark node

    / * * * *@param {*} node* To wrap */
    const parseToDOM = (node) = > {

        const parentNode = node.parentNode
        if (parentNode) {
            const span = document.createElement("span");
            const newNode = node.cloneNode(true);
            span.appendChild(newNode)
            span.className = 'mark'
            parentNode.replaceChild(span, node)
        }
    }

Copy the code

2.2 Serialization and deserialization

serialization


    / * * * *@param {*} textNode 
     * @param {*} Root * starts serializing DOM * */
    const serialize = (textNode, root = document) = > {
        allTextNode = []
        const node = findFatherNode(textNode)
        getAllTextNode(node)
        let childIndexStart = -1
        let childIndexend = -1

        // Calculate the front offset
        const calcLeftLength = (index) = > {
            let length = 0
            for (let i = 0; i < index; i++) {
                length = length + allTextNode[i].length
            }
            return length
        }
        let Index = allTextNode.findIndex(textnode= > textnode === textNode)
        if (Index === 0) {
            childIndexStart = 0     / / before migration
            childIndexend = childIndexStart + textNode.length / / after migration
        } else if (Index === allTextNode.length - 1) {
            childIndexStart = calcLeftLength(Index)
            childIndexend = childIndexStart + textNode.length
        } else {
            childIndexStart = calcLeftLength(Index)
            childIndexend = childIndexStart + textNode.length
        }

        // Locate via its parent node at 😬
        const tagName = node.tagName
        const list = root.getElementsByTagName(tagName)
        // Remove mark's position
        const newList = [...list].filter(node= >node.className ! = ="mark")
        for (let index = 0; index < newList.length; index++) {
            if (node === newList[index]) {
                return { tagName, index, childIndexStart, childIndexend }
            }
        }
        return { tagName, index: -1, childIndexStart, childIndexend }
    }

    / * * * *@param {*} ProNode * retrieves all text nodes, again DFS */
    const getAllTextNode = (proNode) = > {
        if (proNode.childNodes.length === 0) return
        console.log(proNode);
        for (let i = 0; i < proNode.childNodes.length; i++) {
            if (proNode.childNodes[i].nodeType === 3) {
                allTextNode.push(proNode.childNodes[i])
            } else {
                getAllTextNode(proNode.childNodes[i])
            }
        }
    }
Copy the code

deserialization

 / * * * *@param {*} meta 
     * @param {*} Root * deserialize */
    const deSerialize = (meta, root = document) = > {
        const { tagName, index, childIndexStart, childIndexend } = meta
        const parent = root.getElementsByTagName(tagName)[index]
        allTextNode = []
        if (parent) {
            getAllTextNode(parent)
            let nodeIndexStart = -1
            let length = 0
            let length3 = 0
            for (let i = 0; i < allTextNode.length; i++) {
                length = length + allTextNode[i].length
                if (length >= childIndexStart) {
                    nodeIndexStart = i
                    break; }}const calcLeftLength = (index) = > {
                let length = 0
                for (let i = 0; i < index; i++) {
                    length = length + allTextNode[i].length
                }
                return length
            }
            length3 = calcLeftLength(nodeIndexStart)
            return splitNode(parent.childNodes[nodeIndexStart], childIndexStart - length3, childIndexend - length3)
        }
    }
Copy the code

Three: Write to the end

This is just the most basic function of highlighting. I probably won’t update it on Github for some unexplainable reason (depends). End of hydrology

Complete code git address (demo there are many problems, only for reference code. Error: Clear serialization information in local storage, I will optimize the demo after the year.

Recently, I really have no writing at all. The original 1W word is no longer spoken. Now it is very difficult to reach 4K. Well, I’m not that teenager anymore after all

Wish you all a happy New Year in advance, cow batch every day

Reference thanks: github.com/alienzhou/w…