0

I am using following code to highlight text in div. But if I type something easy as "a", "img" or so, it will break the html output, images and break the site.

if ($('#block-multiblock-2 input').val().length !== 0) {
    $('.group-informacie .field-name-body p').each(function() {
        //Handle special characters used in regex
        var searchregexp = new RegExp($("#block-multiblock-2 input").val().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "gi");

        //$& will maintain uppercase and lowercase characters.
        $(this).html($(this).html().replace(searchregexp, "<span class='highlight'>$&</span>"));
    });
}

I think the problem lies within RegExp which has to somehow exclude html tags? I tried inserting <> or so characters which I found in other questions but nothing actually worked.

I am trying to make jquery search within text which is saved by users / ckeditor, which output is sometimes like:

<p><img src="..."/>Some super text <i>here</></p>

So it can contain any html output, headlines, divs, accordions etc.

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
LubWn
  • 150
  • 1
  • 11
  • As long as one just needs to search for single words (or sequences of word characters) which in addition just have to match a text-node's `textContent` this task can be handled by a trivial approach. Anything else, which also does include single words that span several element-/text-nodes, is by far more complex. Which use case does the OP need to find a solution for? – Peter Seliger Jan 01 '21 at 16:08
  • It does not actually matter. I would like to highlight precisely written characters but highlighting whole words would do just fine, I guess. Any idea how to achieve this? Thanks in advance! – LubWn Jan 03 '21 at 00:29
  • I was thinking, maybe only getting text betweens > and <, so only anything outside the tags? But not sure how to do that. Some regex? – LubWn Jan 03 '21 at 00:43

1 Answers1

1

This approach stores a copy (the initial state) of the original element-node where the text search and highlighting is supposed to happen.

With every change of the related search-field's input-value, an entirely new process of searching and possible matching/highlighting within another (always fresh) copy of the original element-node will be triggered.

Every process starts with collecting all valid text-nodes. Each text-node's textContent then gets split by a regex which was created from the current input-value of the related search-field.

The resulting array then gets reduced by an (aggregating) render process which creates either a highlighting element- or a plain text-node with both either replacing the recently/previously processed node or being appended to the latter ...

// node detection helpers.
function isElementNode(node) {
  return (node && (node.nodeType === 1));
}
function isNonEmptyTextNode(node) {
  return (
        node
    && (node.nodeType === 3)
    && (node.nodeValue.trim() !== '')
    && (node.parentNode.tagName.toLowerCase() !== 'script')
  );
}

// dom node render helper.
function insertNodeAfter(node, referenceNode) {
  const { parentNode, nextSibling } = referenceNode;
    if (nextSibling !== null) {

    node = parentNode.insertBefore(node, nextSibling);
  } else {
    node = parentNode.appendChild(node);
  }
  return node;
}

// text node reducer functionality.
function collectNonEmptyTextNode(list, node) {
  if (isNonEmptyTextNode(node)) {
    list.push(node);
  }
  return list;
}
function collectTextNodeList(list, elmNode) {
  return Array.from(
    elmNode.childNodes
  ).reduce(
    collectNonEmptyTextNode,
    list
  );
}
function getTextNodeList(rootNode) {
  rootNode = (isElementNode(rootNode) && rootNode) || document.body;

  const elementNodeList = Array.from(
    rootNode.getElementsByTagName('*')
  );
  elementNodeList.unshift(rootNode);

  return elementNodeList.reduce(collectTextNodeList, []);
}

// highlight functinality.

function createSearchMatch(text) {
  const elmMatch = document.createElement('mark');

  // elmMatch.classList.add("highlight");
  elmMatch.textContent = text;

  return elmMatch;
}

function aggregateSearchResult(collector, text, idx, arr) {
  const { previousNode, regXSearch } = collector;

  const currentNode = regXSearch.test(text)
    ? createSearchMatch(text)
    : document.createTextNode(text);

  if (idx === 0) {
    previousNode.parentNode.replaceChild(currentNode, previousNode);
  } else {
    insertNodeAfter(currentNode, previousNode);
  }
  collector.previousNode = currentNode;

  return collector;
}

function highlightSearch(textNode, regXSearch) {
  // console.log(regXSearch);
  textNode.textContent
    .split(regXSearch)
    .filter(text => text !== '')
    .reduce(aggregateSearchResult, {
      previousNode: textNode,
      regXSearch,
    })
}

function highlightSearchFromBoundContext(/* evt */) {
  const { elmSearch, sourceNode, targetNode } = this;
  const replacementNode = sourceNode.cloneNode(true);

  const searchValue = elmSearch.value.trim();
  if (searchValue !== '') {

    const regXSearchString = searchValue
      // from the OP's original code ... escaping of regex specific characters.
      .replace((/[.*+?^${}()|[\]\\]/g), '\\$&')
      // additional escaping of whitespace (sequences).
      .replace((/\s+/g), '\\s+');
    const regXSearch = RegExp(`(${ regXSearchString })`, 'gi');

    getTextNodeList(replacementNode).forEach(textNode =>
      highlightSearch(textNode, regXSearch)
    );
  }
  targetNode.parentNode.replaceChild(replacementNode, targetNode);

  this.targetNode = replacementNode;
}

// initialize search behavior

function initializeSearchAndHighlight() {
  const elmSearch = document
    .querySelector('#block-multiblock-2 input[type="search"]');
  const elmHighlight = elmSearch && document
    .querySelector('.group-informacie .field-name-body');

  if (elmHighlight && (elmHighlight.textContent.trim() !== '')) {

    const handleChangeEvent = highlightSearchFromBoundContext.bind({
      elmSearch,
      targetNode: elmHighlight,
      sourceNode: elmHighlight.cloneNode(true),
    });
    const handleChangeEventThrottled = _.throttle(handleChangeEvent, 200);

    elmSearch.addEventListener('input', handleChangeEventThrottled);
    
    handleChangeEvent();
  }
}

initializeSearchAndHighlight();
p { margin: 7px 0 0 0; }
/*
.as-console-wrapper { max-height: 67px!important; }
*/
<label id="block-multiblock-2">
  <span class="label">Highlight search ...</span>
  <input
    type="search"
    placeholder="... type some text"
    value="dolor     (sit)     amet"
  />
</label>

<article class="group-informacie">
  <section class="field-name-body">
    <p>
      Lorem ipsum dolor (sit) amet, consetetur sadipscing elitr, ??sed?? diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam [erat], sed diam voluptua.
    </p>
    <p>
      At vero [eos] et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, (no) **sea** takimata sanctus est Lorem ipsum dolor [sit] amet.
    </p>
    <p>
      Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
    </p>
    <p>
      Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
    </p>
  </section>
</article>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>

Promotion

There are related search and highlight answers of mine that have been answered recently and might be useful in order to compare this approach to similar problems ...

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37