skip to Main Content

I try to implement search highlighting functionality. But the problem is that works only when test 1 is chosen. On other pages it doesn’t work and I have no idea why. I suppose that it is because highlight function is called only once and when I choose for example Test 2, it is not called again.
I tried different options, but failed. Since it was a part of a contest, it is important not to change other parts of the code, only final script.

    <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
  </head>
  <style>
    :root {
      --bg-primary: #fbfbfb;
      --bg-secondary: #fff;
      --control-primary: #fdde55;
      --color-primary: #000;
      --depot-color-stroke: rgba(7, 28, 71, 0.12);
    }

    @media (prefers-color-scheme: dark) {
      :root {
        --bg-primary: #111112;
        --bg-secondary: #18181a;
        --color-primary: #fff;
        --depot-color-stroke: rgba(255, 255, 255, 0.12);
      }
    }

    body {
      background-color: var(--bg-primary);
      font-family: Helvetica, Arial, sans-serif;
    }

    header {
      box-shadow: 0 1px var(--depot-color-stroke);
      margin-block-end: 12px;
      padding-block-end: 8px;
    }

    .select-wrapper {
      color: var(--color-primary);
      margin-block-end: 12px;
    }

    .select-wrapper select {
      min-width: 40px;
      cursor: pointer;
      font-size: 20px;
    }

    .search {
      display: flex;
      overflow: hidden;
      flex: 1 1;
      box-sizing: border-box;
      height: 44px;
      border: 2px solid #fc0;
      border: 2px solid var(--control-primary);
      border-radius: 12px;
    }

    .search input {
      flex: 1 1;
      box-sizing: border-box;
      padding-left: 14px;
      font-family: inherit;
      font-size: 16px;
      text-overflow: clip;
      color: var(--color-primary);
      border: 0;
      outline: 0;
      background: initial;
    }

    .card-item {
      padding: 12px 16px;
      border-radius: 16px;
      color: var(--color-primary);
      background-color: var(--bg-secondary);
      box-shadow: 0 4px 12px #0d234308;
    }
  </style>
  <style>
    ::highlight(search-results) {
      background-color: orange;
      text-decoration: underline;
    }

    .search-results {
      background-color: orange;
      text-decoration: underline;
    }
  </style>

  <body>
    <header>
      <div class="select-wrapper">
        <label for="tests-select">Choose test</label>
        <select name="tests" id="tests-select"></select>
      </div>
      <form class="search" role="search" aria-label="Search">
        <input
          id="site-search"
          type="text"
          autocomplete="off"
          aria-label="Query"
        />
      </form>
    </header>

    <div id="root" class="card-item"></div>
  </body>
  <script>
    const rootElement = document.getElementById("root");
    const testsSelect = document.getElementById("tests-select");

    rootElement.addEventListener("onSolutionReady", (event) => {
      const { detail } = event;

      if (detail) {
        testsSelect.innerHTML = detail
          .map((t, index) => `<option value="${t.id}">${index + 1}</option>`)
          .join("");

        rootElement.innerHTML = detail[0].content;

        testsSelect.addEventListener("change", (e) => {
          const test = detail.find((t) => t.id === e.target.value);
          rootElement.innerHTML = test.content;
        });
      }
    });
  </script>
  <script>
    const onSolutionReady = new CustomEvent("onSolutionReady", {
      bubbles: true,
      cancelable: true,
      composed: false,
      detail: [
        {
          id: "f38d0cca-167c-46dc-9504-69ebe13c1e47",
          comment:
            "One text node. Content contains in the middle of a single tag",
          content: `<p>sit amet, Lorem ipsum. Sed non risus</p>`,
          searchFor: "Lorem ipsum",
        },
        {
          id: "20b81641-b065-492d-801a-e786d2a6894b",
          comment: "One text node. Content contains in the end of a single tag",
          content: `<p>Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, Lorem ipsum</p>`,
          searchFor: "Lorem ipsum",
        },
        {
          id: "c8b707f7-91e9-4778-acc6-4f06849bd323",
          comment: "One text node and content contains in a single tag",
          content: `
              <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi.</p>
              `,
          searchFor: "Lorem ipsum",
        },
        {
          id: "eb375bed-bf98-4150-b8ac-711a6c0fe33a",
          comment:
            "The two text nodes and content are contained in sibling tags",
          content: `
              <div><p>Lorem </p><p>ipsum</p></div>
              `,
          searchFor: "Lorem ipsum",
        },
      ],
    });

    document.getElementById("root").dispatchEvent(onSolutionReady);
  </script>

  <script>
    if (!CSS.highlights) {
      document.getElementById("root").innerHTML =
        "CSS Custom Highlight API is not supported. <br />Please, choose another browser. <a href='https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API#browser_compatibility'>More</a>";
    }
  </script>

  <script>
    // Copy paste this script

    const root = document.getElementById("root");
    const siteSearch = document.getElementById("site-search");
    const selectTest = document.getElementById("tests-select");

    const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);

    const allTextNodes = [];

    let currentNode = treeWalker.nextNode();
    while (currentNode) {
      allTextNodes.push(currentNode);
      currentNode = treeWalker.nextNode();
    }

    testsSelect.addEventListener("change", highlight);

    siteSearch.addEventListener("input", highlight);

    function highlight() {
      const str = siteSearch.value.trim().toLowerCase();

      if (!str) {
        CSS.highlights.clear();
        return;
      }

      const ranges = allTextNodes
        .map((el) => {
          return { el, text: el.nodeValue.toLowerCase() };
        })
        .map(({ text, el }) => {
          const indices = [];
          let startPos = 0;
          while (startPos < text.length) {
            const index = text.indexOf(str, startPos);
            if (index === -1) break;
            indices.push(index);
            startPos = index + str.length;
          }

          return indices.map((index) => {
            const range = new Range();
            range.setStart(el, index);
            range.setEnd(el, index + str.length);

            return range;
          });
        });

      const searchResultsHighlight = new Highlight(...ranges.flat());

      // Register the Highlight object in the registry.
      CSS.highlights.set("search-results", searchResultsHighlight);
    }
  </script>
</html>

2

Answers


  1. Make sure to include this block of code inside the highlight() function as well:

    const allTextNodes = [];
    
    let currentNode = treeWalker.nextNode();
    while (currentNode) {
      allTextNodes.push(currentNode);
      currentNode = treeWalker.nextNode();
    }
    

    I faced the same task during the Yandex summer school test. Although I managed to solve it and it worked fine for me, the tester indicated it was incorrect. It appears that the recruiters and seniors were expecting a different algorithm.

    Login or Signup to reply.
  2. The best I could do is make highlighting work on first 3 tests, on the fourth one only one word "Lorem" was being highlighted.

    const root = document.getElementById("root");
    const siteSearch = document.getElementById("site-search");
    const selectTest = document.getElementById("tests-select");
    
    siteSearch.addEventListener("input", highlight);
    selectTest.addEventListener("change", highlight);
    
    function highlight() {
      const article = document.querySelector(".card-item");
    
      // Clear the HighlightRegistry to remove the previous search results.
      CSS.highlights.clear();
    
      // Clean-up the search query and bail-out if it's empty.
      const str = siteSearch.value.trim().toLowerCase();
      if (!str) {
        return;
      }
    
      // Find all text nodes in the article. We'll search within these text nodes.
      const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
      const allTextNodes = [];
      let currentNode = treeWalker.nextNode();
      while (currentNode) {
        allTextNodes.push(currentNode);
        currentNode = treeWalker.nextNode();
      }
    
      // Concatenate all text nodes' content to find matches across nodes.
      const fullText = allTextNodes.map(node => node.textContent.toLowerCase()).join("");
      const indices = [];
      let startPos = 0;
      while (startPos < fullText.length) {
        const index = fullText.indexOf(str, startPos);
        if (index === -1) break;
        indices.push(index);
        startPos = index + str.length;
      }
    
      // Create a range object for each instance of str we found in the full text.
      const ranges = [];
      indices.forEach(index => {
        let count = index;
        for (const node of allTextNodes) {
          const nodeLength = node.textContent.toLowerCase().length;
          if (count >= nodeLength) {
            count -= nodeLength;
          } else {
            const range = new Range();
            range.setStart(node, count);
            // Ensure the range does not exceed the node's length
            const endOffset = count + str.length > nodeLength ? nodeLength : count + str.length;
            range.setEnd(node, endOffset);
            ranges.push(range);
            break;
          }
        }
      });
    
      // Flatten the ranges array and create a Highlight object for the ranges.
      const searchResultsHighlight = new Highlight(...ranges.flat());
    
      // Register the Highlight object in the registry.
      CSS.highlights.set("search-results", searchResultsHighlight);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search