Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
190 views
in Technique[技术] by (71.8m points)

javascript - What is a good enough approach for writing real-time text search and highlight functionality which does not break the order of text- and element-nodes

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...