Find prefix, suffix and coincidents in javascript

I am trying to create a script in Javascript that performs markup functions, similar to the browser's ctr + f function. It shouldn't be difficult, but I have to check four criteria that I check on a switch, the criteria are "Matches", "Starts with" , "Ends with" and "Any position". I'm using the window.find() method, but I seem to be having trouble with it. In other words, I need to be able to highlight "Lor" for the "Starts with" criteria and "em" for the "Ends with" criteria and of course "Lorem" for the "Coincident" criteria.

Can someone help me?

function HighLightText() {

    var contentElement = document.getElementById("content");
    let word = document.getElementById("word").value;
    let className = "search-mark";
    let criterial = "Any position"; //Can be "Ends with" | "Starts with" | "Coincident".
    let scrollDetail = false;
    
    var searchResults = document.getElementsByClassName(className);
    

    if (window.find && window.getSelection) {
        document.designMode = "on";

        var sel = window.getSelection();
        sel.collapse(document.body, 0);
        var listOfWord = [];
        switch (criterial) {
            case "Coincident":     
                  listOfWord.push(" " + word + ""); 
                listOfWord.push(word + " "); 
               // ...more cases where the word is found
                break;
            case "Any position":
                listOfWord.push(word);
                break;
            case "Ends with":
                listOfWord.push(word + ".");
                listOfWord.push(word + ",");
                listOfWord.push(word + "!");
                listOfWord.push(word + "?");
                // ...more cases where the word is found
                break;
            case "Starts with":
                listOfWord.push(" " + word);
                // ...more cases where the word is found
                break;

        }

        listOfWord.forEach(singleWord => {
            while (window.find(singleWord)) {  
                scroll(0, scrollDetail)
                document.execCommand("insertHTML", false, '<b><mark>' + window.getSelection().toString() + '</mark></b>');
            }
        });
        
        sel.collapseToEnd();
        document.designMode = "off";
    } else if (document.body.createTextRange) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(contentElement);
        while (textRange.findText(text)) {
            
            textRange.execCommand("BackColor", false, "yellow");
            textRange.collapse(false);
        }
    }
}

Here is my fiddle: https://jsfiddle.net/MadDev82/97sdn0mr/4/

Thanks!


As window.find is deprecated, I'd manipulate the DOM textNodes directly. Something like this:

function searchTextNodes(target, ref) {
  // target: Element to search from <HTMLElement>
  // ref:    Keyword or RegExp to search for <String|RegExp>
  let inNode = 0;
  const key = (ref instanceof RegExp) ? ref : new RegExp(ref),
    matches = [],
    references = [],
    // Store the matching nodes and the matched strings
    textNodes = Array.from(target.childNodes).filter(node => {
      key.lastIndex = 0;
      if (node.nodeType === 3 && key.test(node.data)) {
        matches.push(node.data.match(key));
        return true;
      }
    });
  // Split the nodes of target to match the matches
  for (const textNode of textNodes) {
    let node = textNode;
    for (const text of matches[inNode]) {
      const start = node.data.search(key);
      node.splitText(start + text.length);
      const currentRef = node.splitText(start);
      references.push(currentRef);
      node = currentRef.nextSibling;
    }
    inNode += 1;
  }
  return references;
}

const modes = {
  prefix: (key, flags) => new RegExp(`(?<=\\b)(${key})(?=\\w)`, flags),
  suffix: (key, flags) => new RegExp(`(?<=\\w)(${key})(?=\\b)`, flags),
  word: (key, flags) => new RegExp(`\\b(${key})\\b`, flags),
  any: (key, flags) => new RegExp(`(${key})`, flags)
}

function reset(content) {
  const marks = content.querySelectorAll('mark');
  marks.forEach(mark => {
    const textNode = document.createTextNode(mark.textContent),
      parent = mark.parentNode;
    parent.replaceChild(textNode, mark);
    parent.normalize();
  });
}

function search() {
  const content = document.querySelector('.content');
  reset(content);
  const regexSpecials = /([\^\$.:=*?+()[\]{}|\\])/g,    
    elements = content.querySelectorAll('*'),
    searchMode = document.querySelector('input[name="mode"]:checked').value,
    keyword = document.querySelector('[name="keywords"]').value.replace(regexSpecials, '\\$1'),
    flags = (document.querySelector('[name="case"]').checked) ? 'gi' : 'g',
    regex = modes[searchMode](keyword, flags);
  
  elements.forEach(element => {
    if (element.nodeType > 3 || element.tagName === 'SCRIPT') {return;} // Next, exclude scripts, comments etc.
    const nodes = searchTextNodes(element, regex);
    nodes.forEach(node => {
      const mark = document.createElement('mark');
      mark.textContent = node.textContent;
      element.replaceChild(mark, node);
    });
  });
}

const searchBut = document.querySelector('button');
searchBut.addEventListener('click', search);
<input type="text" name="keywords" value="text">
<button>Search</button>
<br>
<label><input type="radio" name="mode" value="prefix"> Prefix</label>
<label><input type="radio" name="mode" value="suffix"> Suffix</label>
<label><input type="radio" name="mode" value="word" checked> Word</label>
<label><input type="radio" name="mode" value="any"> Any</label>
<label><input type="checkbox" name="case" checked> Ignore case</label>

<hr>
<div class="content">
  <h1>Text search</h1>

  <p>A test text for searching TextNodes from a text.</p>
  <p>This paragraphtext contains more than one text-word, the textsearching engine should find all the text from this text.</p>
  <p>This word is unique within the testtext to search for.</p>
</div>

The idea of the code is to extract the keyword(s) into their own textNodes within the specified element (.content in this case), and then wrap the new nodes with <mark>. The advantage is, that you can select an element to search from, it doesn't have to be the entire document.

searchTextNodes finds the keywords and extracts them into textNodes in a single element. The function is called from the iteration of all the elements within the specified element. The "UI" handling is integrated in search function, but if needed, it's easy to pull out and parametrize the function instead.

searchTextNodes is a generic textNode finder, which you can use outside of this snippet where needed.


You can get an idea from the Hilite NPM package (Disclaimer:Author) which highlight() method has a check for the desired match criteria:

//...

const val_escaped = regEscape(this._value);

const val_criteria = {
  any: val_escaped,
  start: `(?<=^| )${val_escaped}`,
  end: `${val_escaped}(?=$| )`,
  full: `(?<=^| )${val_escaped}(?=$| )`,
}[this._criteria];

this._reg = new RegExp(val_criteria, this.sensitive ? "g" : "ig");

//...

where for a (reg-escaped!) value:

  1. if the criteria is "any" the value is used as is
  2. if the criteria is "start" the lookbehind regex (?<=^| ) is prepended to the value
  3. if the criteria is "end" the lookahead regex (?=$| ) is appended to the value
  4. if the criteria is "full" (as for full match) both the lookbehind and lookahead are added to the string

which is than passed to the new RegExp constructor.

Demo:

<input value="" type="search" id="search" placeholder="Find" autocomplete="off" />
<select id="criteria">
  <option disabled>Select match criteria: </option>
  <option value="any" selected>Any</option>
  <option value="start">Starts with</option>
  <option value="end">Ends with</option>
  <option value="full">Full match</option>
</select>
<label><input type="checkbox" id="sensitive"> Case sensitive</label>

<div id="area">Highlight height right<br>ribbon Rough hi high, HI!</div>

<script type="module">
  import { Hilite } from "https://unpkg.com/@rbuljan/[email protected]";
  
  const HL = new Hilite("#area");
  const EL_search = document.querySelector("#search");
  const EL_criteria = document.querySelector("#criteria");
  const EL_sensitive = document.querySelector("#sensitive");

  // Do on search "input"
  EL_search.addEventListener("input", () => HL.value = EL_search.value);
  // Change match criteria
  EL_criteria.addEventListener("input", () => HL.criteria = EL_criteria.value);
  // Change case
  EL_sensitive.addEventListener("input", () => HL.sensitive = EL_sensitive.checked);
</script>