2

I change the text color of matched words as the user type (He types in a contenteditable div). If he types the matching word, I create a span element and add a class name to it (this class has the styles via CSS).

The problem is that when there is a match, and the user keeps typing, he is inside that newly inserted span and he keeps writing with the new style (so if for example the new style is blue text color, the text will remain blue after the match).

But I want to wrap only that matched word with the span element.

Edit: My goal is to be able to toggle styles once there is a match, and if the user writes again inside that match then recheck it if it still matches. If not, completely remove that styling.

(Example: User writes "Let's find for the matching word", if the word is "word", then highlight this word and be able to click it. If he then edits the text to "Let's find for the matching wosdfsdfrd" then remove the styling from that word)

This is what I currently have. Sometimes it just leaves a <font> tag with color style that I never added: (You can change the searched word by editing the searchTerm variable. Originally I used regex, but for simplicity it's a string)

const processChange = debounce(() => replace());

function debounce(func, timeout = 300) {
    let timer;
    return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
        func.apply(this, args);
    }, timeout);
    };
}

function replaceText(textNode) {
    let textDiv = document.getElementById("textarea");
    let textContent = textNode.textContent;
    let searchTerm = 'word';
    let indexOfFirst = textContent.indexOf(searchTerm);

    if (indexOfFirst >= 0) {
        if (textNode.parentNode.nodeName.toLowerCase() != 'span') {
            let subTextNode = textNode.splitText(indexOfFirst);
            let span = document.createElement('span');
            span.classList.add("match-found");
            span.setAttribute("onclick", "toggleMatch(this)");
            span.appendChild(document.createTextNode(searchTerm));
            textDiv.insertBefore(span, subTextNode);
            subTextNode.deleteData(0, searchTerm.length);
            } else {
                console.log("..");
            }
    }
}

// get all text nodes
function textNodesUnder(el) {
    var n, a = [],
    walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
    while (n = walk.nextNode()) a.push(n);
    return a;
}

function replace() {
    let textDiv = document.getElementById("textarea");
    let a = textNodesUnder(textDiv);
    a.forEach(element => replaceText(element));
}

function matchFound(e) {
    e.setAttribute("onclick", "toggleMatch(this)");
    e.className = 'match-found';
}

function toggleMatch(e) {
    e.setAttribute("onclick", "matchFound(this)");
    e.className = 'toggle-match';
}
#textarea {
            -moz-appearance: textfield-multiline;
            -webkit-appearance: textarea;
            border: 1px solid gray;
            font: medium -moz-fixed;
            font: -webkit-small-control;
            height: 130px;
            overflow: auto;
            padding: 2px;
            resize: both;
            width: 400px;
        }

        .match-found {
            color: red;
            cursor: pointer;
        }

        .toggle-match {
            color: green;
            cursor: pointer;
        }
<div id="textarea" contenteditable="true" onkeyup="processChange()"></div>
B.D Liroy
  • 33
  • 6
  • How are matched words determined? You say *"... as the user types..."* do you have event handlers for key events? – zer00ne Feb 13 '22 at 20:32
  • I have a debounce function(on key up) that triggers a search function that searches for matching strings with regex amongst all the text nodes inside that `contenteditable` `div` – B.D Liroy Feb 13 '22 at 20:40
  • If the debounce function is an event handler binding to key events, modify it so it will recheck for matches on every keystroke. ATM, it doesn't do it at every keystroke because if it did, you wouldn't have this problem. – zer00ne Feb 13 '22 at 20:43
  • That's what I do (or at least that's what I think my code is supposed to do), but it's not working. Should I post the entire code? – B.D Liroy Feb 13 '22 at 20:45
  • Yeah, just don't bloat your OP with code (I know it's hard to guage). I believe that's your best bet. I have to go, but I'll bookmark your post and review it later on in the day, good luck. – zer00ne Feb 13 '22 at 20:49
  • Thank you. I added the snippet and some explanation – B.D Liroy Feb 13 '22 at 21:06
  • 2
    You'd better remove all markers and reintroduce them, because a user could also put their cursor in the middle of a marked word and start typing there, and you'd want to remove the marker then. All kinds of boundary cases can occur, which is why it will be easier to remove the markers at least in the location where the last modifications were happening. – trincot Feb 13 '22 at 21:22
  • This is one of the problems I'm working on right now (where a user starts typing in the middle of a marker/span), but I did not save any reference to that marked text node (if that's even possible, I am learning as I write this). What is the best way to do that in my case then? Add a listener to the marker? But then when the user starts typing how do I remove the markers without interrupting the user? – B.D Liroy Feb 13 '22 at 21:33
  • Have a look at [Set cursor after span element inside contenteditable div](https://stackoverflow.com/q/15813895/1048572), [Inserting caret after an inserted node](https://stackoverflow.com/q/9828623/1048572), [Set caret position right after the inserted element in a contentEditable div](https://stackoverflow.com/q/4834793/1048572) and all the related questions. If none of these work, would it be a reasonable workaround to only match full words, i.e. highlight the match only after the user typed a whitespace or punctuation mark? – Bergi Feb 13 '22 at 22:32
  • I will take a look, and yes this is a good workaround, because I have a denounce function anyway so it doesn't check immediately when there's a match all the time (if the user keeps typing after the match in the time defined by the denounce function it will be delayed by that amount) – B.D Liroy Feb 13 '22 at 23:24

0 Answers0