Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PrismJS: Centering Highlighted Lines Vertically

PrismJS is a syntax highlighter for source code in Web pages. It has a line-highlight plugin that highlights certain lines in the source code. In particular, that plugin supports determining what lines to highlight from the hash (e.g., #play.5-6 says to highlight lines 5 through 6 of the <pre> element with an id of play).

For that hash, the plugin uses scrollIntoView() to ensure that the highlighted lines are visible:

document.querySelector('.temporary.line-highlight').scrollIntoView();

(here line-highlight is added as a class to the lines)

However, scrollIntoView() puts the highlighted lines at the top. In my use case, it would be better to have the highlighted lines be vertically centered, assuming that the highlighted range is shorter than the available space.

What would I replace that above line with, in order to have those highlighted lines be vertically centered?

FWIW:

  • While I'm OK with CSS/JS, I'm not an expert

  • If it matters, my use case is to show this code in a WebView widget in an Android app

like image 389
CommonsWare Avatar asked Oct 19 '22 16:10

CommonsWare


1 Answers

Here is a function that will center the highlighted lines when scrolling (assuming that the scrollbar is on the pre element):

function scrollToLines (pre) {
  var lines = document.querySelector('.temporary.line-highlight'),
      linesHeight = lines.offsetHeight,
      preHeight = pre.offsetHeight;

  lines.scrollIntoView();

  if (preHeight > linesHeight && pre.scrollTop < (pre.scrollHeight - preHeight)) {
    pre.scrollTop = pre.scrollTop - (preHeight / 2) + (linesHeight / 2);
  }
}

Just call this function in the appyHash function after the highlightLines function is called:

function applyHash() {
  // ...

  highlightLines(pre, range, 'temporary ');
  scrollToLines(pre);
}

Explanation:

Just like before, the scrollIntoView() method is invoked and the pre element is scrolled to the top of the first highlighted line.

If both of these conditions are true...

  • preHeight > linesHeight - The height of the highlighted range is less than the height of the pre element and:
  • pre.scrollTop < (pre.scrollHeight - preHeight) - The pre element wasn't scrolled to the bottom (i.e., the current scroll position is less than the available scrollable height minus the height of the pre element).

...then subtract half of the pre element's height from the current scroll position and add half of the highlighted line's height. In doing so, the highlighted lines will be centered vertically if the range's height doesn't exceed the height of the pre element.

Basic test cases based on the live snippet that I was using when writing the code:

  • #scroll.40-44, #scroll.107, #scroll.40-62, #scroll.4-6, #scroll.140-144

As you pointed out in the comments, this wasn't working for you because the scrollbar was actually on the body element (rather than the pre element like above).

To resolve this, change the logic so that the calculations are relative to the body element and window:

function scrollToLines () {
  var lines = document.querySelector('.temporary.line-highlight'),
      linesHeight = lines.offsetHeight,
      body = document.body,
      windowHeight = window.innerHeight;

  lines.scrollIntoView();

  if (windowHeight > linesHeight && body.scrollTop < (body.scrollHeight - windowHeight)) {
    body.scrollTop = body.scrollTop - (windowHeight / 2) + (linesHeight / 2);
  }
}

Here are some basic test cases demonstrating that it works when the highlighted range is at the top/bottom or if the height of the highlighted range exceeds the height of the window:

  • #src.42-58, #src.2-9, #src.34-40, #src.15-80, #src.80-88

Since you may not know whether the scrollbar will be on the pre element or the body, you could check if the pre element is scrollable first and then fall back to making the calculations relative to the body element if it isn't:

function scrollToLines (pre) {
  var lines = document.querySelector('.temporary.line-highlight'),
      linesHeight = lines.offsetHeight,
      pre = pre.scrollHeight > pre.clientHeight ? pre : document.body,
      preHeight = pre === document.body ? window.innerHeight : pre.offsetHeight;

  lines.scrollIntoView();

  if (preHeight > linesHeight && pre.scrollTop < (pre.scrollHeight - preHeight)) {
    pre.scrollTop = pre.scrollTop - (preHeight / 2) + (linesHeight / 2);
  }
}
like image 200
Josh Crozier Avatar answered Nov 03 '22 05:11

Josh Crozier