Search code examples
csstexthighlight

Search highlighting (CSS Highlight API)


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>

Solution

  • 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.