1

How to get first visible DOM element currently showing on the screen ?

I tried something like

var el = document.elementFromPoint(x, y)

and increasing the y coordinates in a while loop, but the problem is it does not work when there are css multi columns on the document, in that case the < html > tag is returned, not the actual element. Is there any way I can really get the element on the (top,left) of the screen which works for css columns ?

CodeWeed
  • 971
  • 2
  • 21
  • 40

2 Answers2

4

This is a very basic example that just gets the element that is the furthest to the top. It's just a start, because you may want to take into consideration the left offset as well.

You'll want to exclude the body and html elements (as shown below), since they will always be first.

Fiddle

var first;
$(':visible').not('body, html').each(function() {
    if (typeof first == 'undefined') {
        first = $(this);
    } else if ($(this).offset().top < first.offset().top) {
        first = $(this);
    }
});
first.addClass('highlight');
Community
  • 1
  • 1
Aaron Blenkush
  • 3,034
  • 2
  • 28
  • 54
  • +1, with the caveat that if you care about the left too, you'll need to add that just like the top is checked here. – cHao Mar 14 '13 at 16:59
  • @Aaron Blenkush I think I will try this out and modify it to what I need. thanks – CodeWeed Mar 14 '13 at 17:01
  • 1
    @CodeWeed, I just realized as well: this won't take into account the scrolled position of the window. If that is a factor, you'll need to take that into consideration (subtract the `window.scrollY` from the offsets). You may also need to consider the height of each element -- you may scroll so that an element is off the edge of the window, but the bottom portion may still be visible. In that case you'd *add* the height of the element back in to get the "position" of the bottom edge of the element in relation to the window. Then you'd just get the smallest value that's not less than zero. – Aaron Blenkush Mar 14 '13 at 17:08
  • 1
    @CodeWeed: Actually, if you needed to take into account scrolling, you'd most likely need to implement an "overlap" formula, to see if the element overlaps with the window at all AND is `:visible` AND is closest to the top left. Don't know how complicated you want to go with this! – Aaron Blenkush Mar 14 '13 at 17:14
  • @AaronBlenkush Thanks I appreciate your input. – CodeWeed Mar 14 '13 at 17:31
0

A good idea, especially if you have many elements, is to do a binary search. Here is a complete example. The goal of this code is to highlight in a sidebar the headings of sections which are currently visible in <main>.

const asideTocItems = Array.from(asideToc.querySelectorAll('li'))
    .map(item => ({ item: item, heading: document.getElementById(getItemTargetId(item)) })),
  mainBlock = document.querySelector('main');
function updateAsideToc() {
  // Binary search to find the first heading which is above the top
  let searchTop = 0, searchBot = asideTocItems.length;
  while (searchTop + 1 < searchBot) {
    const searchMid = (searchTop + searchBot) >> 1, heading = asideTocItems[searchMid].heading;
    if (heading.offsetTop - getElemFontSize(heading) <= mainBlock.scrollTop) {
      searchTop = searchMid;
    } else {
      searchBot = searchMid;
    }
  }
  // Previously marked items are unmarked
  for (const item of asideToc.querySelectorAll('li.current')) {
    item.classList.remove('current');
  }
  // We mark items starting from the found one
  const visibleBottomY = mainBlock.scrollTop + mainBlock.clientHeight;
  for (let itemNum = searchTop; itemNum < asideTocItems.length; ++itemNum) {
    // If the current heading is below the bottom, it’s time to leave
    const heading = asideTocItems[itemNum].heading;
    if (heading.offsetTop + getElemFontSize(heading) >= visibleBottomY) {
      break;
    }
    let item = asideTocItems[itemNum].item;
    item.classList.add('current');
    // Parents are expanded
    do {
      item.classList.remove('collapsed');
    } while ((item = item.parentElement.closest('li')) != null);
  }
  // Previously expanded items are collapsed back, unless they still have current items
  for (const item of asideToc.querySelectorAll('.hasChild:not(.current):not(.collapsed)')) {
    if (item.querySelector('.current') == null) {
      item.classList.add('collapsed');
    }
  }
  // We scroll the sidebar so that all marked items are visible
  const currentItems = asideToc.querySelectorAll('.current > a');
  if (currentItems.length > 0) {
    const first = currentItems[0], last = currentItems[currentItems.length - 1],
      container = asideToc.closest('aside');
    if (first.offsetTop < container.scrollTop) {
      container.scrollTop = first.offsetTop;
    } else if (last.offsetTop + last.offsetHeight > container.scrollTop + container.clientHeight) {
      container.scrollTop = last.offsetTop + last.offsetHeight - container.clientHeight;
    }
  }
}
mainBlock.onscroll = updateAsideToc;
updateAsideToc();

It is for the HTML manual of a program of my teacher. It is currently live here but should be moved here soon.

I may miss some interesting or more optimized features of JavaScript. It is so complicated…!

As it is a common feature on websites, I am curious to know if they use that same technique or if they have a better (or worse?) one.