JavaScript - detecting what the user searches on the page

We usually believe JavaScript running in the browser does not have access to some external information, but there can be some tricks how to obtain it. In this article, I will show how we can sometimes get the text the user types into the browser’s Search on page (Ctrl + F) field.

Try it

Firstly, you can directly try it here (it should work in Chromium-based desktop browsers).

Simply refresh this page (F5), press Ctrl + F and start typing.

You searched for:

If it does not work, please refresh the page and try again. Do not type too fast and do not use any special characters, it does not work then. Explanations of the limitations are below.

How it works

  1. I created a div (let’s name it search_detector) positioned somewhere outside of the page (so that it is not visible).
  2. I filled the search_detector with all letters (a - z, 0 - 9, some special characters), each letter on a separate line.
  3. I set up the search_detector to be scrollable and set up some small height for it (e.g. 1px).
  4. When the user starts searching and types the first letter, the letter is found in search_detector, and search_detector is scrolled to a position where the letter is visible.
  5. I handle the onscroll event and get the scroll position (scrollTop). Since I know the height of each line, I can calculate which letter was selected.
  6. If the user searched e.g. for letter x, I re-fill search_detector with x + all letters, so with xa, xb, xc etc. In this state, it waits for the user to type the second letter (and it then continues with step 4).

Some details

  • I had to take special care of the CSS properties applied to search_detector and the lines inside it. Current page styles can mess up with borders, paddings and fonts, which can break the detection.
  • Since the search in the browsers tends to be case-insensitive, I added only lowercase letters to search_detector.

Browsers

  • I tried it with Chromium version 79.0.3945.79 on Linux. There it worked.
  • edit 2020-01-12 In Opera (Version 65.0.3467.69) it works, too. Maybe all Chromium-based desktop browsers will work.
  • edit 2020-01-12 But on Chrome in Android, it does not work. It animates the scrolling while going to the occurrence. Our detection mechanism captures all characters encountered on the way, before finally getting the correct character.
  • edit 2020-01-12 In IE 11 it works, too.
  • It did not work for me in Firefox (version 71.0). If the text being searched happens to be found on other places in the page, too, Firefox rather pre-select that (already visible) occurrence, instead of the occurrence in search_detector (it somehow penalizes the occurrences where scrolling would be needed to select them). But, of course, after pressing F3 we can get into the occurrence in search_detector. On a blank page without any other text, it works in Firefox, too (e.g. try the code below, it is an example of such blank page where it works).

Limitations

  • Typing too fast breaks the detection - maybe faster typing causes that some events are handled in a different order.
  • Special characters do not work. For this demonstration I only filled search_detector with some common characters.
  • It works only for the first typing - correcting the typed text (e.g. removing characters, pressing backspace, starting the search again) interrupts the detection. Maybe it is at least possible to restart the detection mechanism when pressing of Ctrl+F is detected.
  • It works only for typing one letter at a time (it does not work when the text is pasted to the search field).
  • It breaks when the search box was already pre-filled with one letter the user searched before, and we type the same letter.
  • It does not work when the user clicked somewhere on the page before performing the search. It looks like the click sets up the position from which the search will be performed (the occurrence after this position will be pre-selected, instead of the occurrence in search_detector).
  • The functionality can depend on the browser - see above.

Conclusion

The method described above is very fragile, but I believe with some tweaks, we could overcome at least some part of the limitations.

There can be other methods, e.g. guessing the text being searched in a long article from changes of scrollTop and the article text (when scrollTop changes, we can assume that the part of the article at this position contains something that the user typed). Analyzing data from more scrollTop changes (as the user types to the search field) can lead to some good guess.

I am not sure whether browsers should block this behavior and how.

Maybe some ideas:

  • do not scroll anything on the page while the user is typing (only pre-select the already visible text if such was found - like Firefox does - see above) - allow scrolling to some occurrence only after the user presses Enter, or only after some count of letters was typed (it would be difficult to prepare all combinations of e.g. 8 letters in search_detector)
  • do not scroll anything on the page without some additional user interaction (e.g. user would have to select the scrollable element where the search should be performed)

Of course, overriding the Ctrl + F functionality and creating our own search field directly in our page gives us direct access to the text being searched.

Example code

The whole example HTML file:

<html>
  <head>
    <script>
      function clearElement(element) {
        while (element.firstChild) {
          element.removeChild(element.firstChild);
        }
      }

      function prepareSearchDetector() {
        var div = document.createElement("DIV");
        div.setAttribute("id", "search_detector");
        div.setAttribute("style", "overflow-y: scroll; height: 1px; position: absolute; left: -100px; top: -100px; margin: 0px; padding: 0px; border: 0; box-sizing: content-box; display: block;");
        document.body.insertBefore(div, document.body.firstChild);
      }

      function addDivWithPossibleString(parent, str) {
        var div = document.createElement("DIV");
        div.setAttribute("style", "height: 10px; margin: 0px; padding: 0px; border: 0; box-sizing: content-box; display: block; font: initial; font-size: 8px");
        var text = document.createTextNode(str);
        div.appendChild(text);
        parent.appendChild(div);
      }

      function updateSearchDetector(prefix) {
        var searchDetector = document.getElementById("search_detector");
        clearElement(searchDetector);
        addDivWithPossibleString(searchDetector, prefix);
        for (var char = 32; char <= 64; ++char) {
          addDivWithPossibleString(searchDetector, prefix + String.fromCharCode((char)));
        }
        for (var char = 91; char <= 126; ++char) {
          addDivWithPossibleString(searchDetector, prefix + String.fromCharCode((char)));
        }
      }

      function prepareScrollHandler() {
        var searchDetector = document.getElementById("search_detector");
        var detected = document.getElementById("result");

        searchDetector.onscroll = function(e) {
          var scrollPos = e.target.scrollTop;
          if (scrollPos === 0) {
            return;
          }

          var scrolledToIndex = Math.floor(scrollPos / 10);
          var scrolledToChild = searchDetector.childNodes[scrolledToIndex];
          var searchedFor = scrolledToChild.innerText;

          updateSearchDetector(searchedFor);

          clearElement(detected);
          var text = document.createTextNode(searchedFor);
          detected.appendChild(text);
          searchDetector.scrollTop = 0;
        }
      }

      window.onload = function() {
        prepareSearchDetector();
        updateSearchDetector("");
        prepareScrollHandler();
      }
    </script>
  </head>
  <body>
    You searched for:
    <div id="result"></div>
  </body>
</html>
Written on January 11, 2020