2

Situation:
Suppose we are reading the content somewhere down the page that is built to be responsive. Suppose also that we resize the browser window to a smaller size and that some content above get extended down due to the thinner width, hence making the whole page longer. Then as we resize, whatever content we are looking at will get pushed down the page accordingly.

Example:
Suppose we were to look at the Helper classes section in this page. Then shrinking/expanding the window a sufficient amount moves the bit we were reading down/up the current view.

Prompt:
Is there any way we can fix this? I.e. maintain our current view of the page regardless of what happens to the contents above it when we resize the window.

Thoughts:
I am thinking that we could at least start with javascript and put an event on window resize. Then automatically scroll the page to the top-most element that was in our view on event fire. I don't know how this will affect the performance, however, especially in bigger pages.
There's also the problem of refering to the top-most element in current view. The top of our current view might be cutting off the top portion of some elements, not to mention that there's usually more than 1 element layered on top of one another at any point within the page. The notion of top-most element I've mentioned is not very well-defined :(

Also rather than a problem of responsive design in general, instead it seems to me like this is a problem with the default scrolling behaviour of web browsers? Or perhaps I am missing some circumstances where the current behaviour is desirable.


Edit 2 4

Updated fiddle (see fullscreen result) based on Rick Hitchcock's solution's solution.

With jQuery:

//onresize:
var scrollAmount;

if (topNode.getBoundingClientRect().top >= 0) {
    scrollAmount = $(topNode).offset().top - topNode.getBoundingClientRect().top;
} else {
    scrollAmount = $(topNode.offset().bottom - topNode.getBoundingClientRect().bottom;
}
$(window).scrollTop(scrollAmount);

The fiddle is acting a bit weird even in the same browsers, I've uploaded the same script using a free hosting here.
Still need to incorporate the IE, Opera and Safari fix for elementFromPoint.


Edit 3

Thanks for all the help, Rick Hitchcock. Welcome to stackoverflow, by the way :)
The discussion is turning into cross-browser compatibility issues so I've accepted your answer since we've pretty much got the answer to the original question. I'll still be fixing up my implementation though. The focus being cross-browser issues, topNode criteria, and topNode cut-off handling.

An edge case

While playing around with it, I noticed that when we were at the bottom of the page in a small viewport, then switch to a larger viewport (let us assume now that some more elements that were originally above the element we saw now came into view due to shorter container from wider viewport) the window cannot always lock the topNode to the top of the viewport in such a case since we've reached the scroll bottom. But then switching back to the small viewport now uses a new topNode that got into the viewport during the switch.
Although this should be expected from the behaviour being implemented, it is still a weird side-effect on scroll bottom.

I will also be looking into this in due course. Initially, I am thinking of simply adding a check for scroll bottom before we update topNode. I.e. to keep the old topNode when we've reached scroll bottom until we've scrolled up again. Not sure how this will turn out yet. I'll make sure to see how Opera handle this as well.

Community
  • 1
  • 1
Sylin
  • 201
  • 2
  • 9

1 Answers1

2

Here's what I've come up with:

(function(){
   var topNode;

   window.onscroll=function() {
     var timer;
     (function(){
        clearTimeout(timer);
        timer= setTimeout(
                 function() {
                   var testNode;
                   topNode= null;
                   for(var x = 0 ; x < document.body.offsetWidth ; x++) {
                     testNode= document.elementFromPoint(x,2);
                     if(!topNode || testNode.offsetTop>topNode.offsetTop) {
                       topNode = testNode;
                     }
                   }
                 },
                 100
               )
      }
     )();
   }

   window.onresize=function() {
     var timer;
     (function(){
        clearTimeout(timer);
        if(topNode) {
          timer= setTimeout(function(){topNode.scrollIntoView(true)},10);
        }
      }
     )();
   }
 }
)();

If there were a window.onbeforeresize() function, this would be more straightforward.

Note that this doesn't take into account the scrolled position of the element's textNode. We could handle that if only the height of the window were resized. But resizing the width would generally cause reformatting.

This works in Chrome, Firefox, IE, and Safari.

Edit

How it works

The code's closures make variables private, and the timers prevent the code from running constantly during scrolling/resizing. But both tend to obfuscate the code, so here's another version, which may aid in understanding. Note that the onscroll timer is required in IE, because elementFromPoint returns null when it used in onscroll event.

var topNode;

window.onscroll=function() {
  setTimeout(
    function() {
      var testNode;
      topNode= null;
      for(var x = 0 ; x < document.body.offsetWidth ; x++) {
        testNode= document.elementFromPoint(x,2);
        if(!topNode || testNode.offsetTop>topNode.offsetTop) {
          topNode = testNode;
        }
      }
    },
    100
  )
}

window.onresize=function() {
  if(topNode) {
    topNode.scrollIntoView(true)
  }
}

topNode maintains the screen's top-most element as the window scrolls.

The function scans the screen left to right, along the 3rd row: document.elementFromPoint(x,2)*

It doesn't scan along the 1st row, because when IE does scrollIntoView, it pushes the element down a couple pixels, making the top-most screen element the previous element. (Figured this out through trial and error.)

When the window is resized, it simply positions topNode at the top of the screen.

[*Originally, onscroll scanned left to right along the 11th row (in pixels) until it found an element with just one child. The child would often be a textNode, but that wouldn't always be the case. Example:

<div><ul><li>...<li>...<li>...</ul></div>

The div has only one child – the ul. If the window were scrolled to the 50th li, scanning left to right would incorrectly return the div due to the inherent padding of lis.

The original code has been updated. ]

Community
  • 1
  • 1
Rick Hitchcock
  • 35,202
  • 5
  • 48
  • 79
  • Hmm, this is very interesting and it's a lot harder than I thought. Am I correct in understanding that here, `topNode` is defined to be the first element (going from left to right of the viewport) with exactly 1 child on the line y=-10 where the origin is the top-left corner of the viewport? [fiddle with dummy content](http://jsfiddle.net/357De/9/) – Sylin Aug 06 '14 at 02:02
  • Oh! Could we perhaps adjust the scroll position on `topNode` with `topNode.getBoundingClientRect()`? As scrolling updates `topNode`, we also keep track of its off-screen/on-screen ratio. I'll update in the question above. – Sylin Aug 06 '14 at 09:14
  • You're absolutely correct about topNode being the first element with exactly 1 child. I now realize that the "one-child" policy won't always work, so I've edited the code. I've also added a How it works section. – Rick Hitchcock Aug 06 '14 at 14:15
  • Look forward to trying your getBoundingClientRect ideas, but I'd better get to work! – Rick Hitchcock Aug 06 '14 at 14:17
  • Oh wow! Thanks for the explanation! I will study them tomorrow morning and see what I make of it :) Plus my own fiddle omitted the timer stuff because I didn't understand how they work yet and I thought you added them for performance reasons only and didn't know it enhances compatibility, good ol' IE. character limit: – Sylin Aug 06 '14 at 14:42
  • I've played with getBoundingClientRect a bit by maintaining the ratio I first mentioned, and then again but maintaining the length from bottom of topNode to the top of viewport instead. Both make the adjustments with scrollBy after the initial scroll to topNode. They were both very buggy (scrolling to the element before, different behaviour when shrinking vs expanding etc.) and blinking when I tried them, although I think it's just my code and it could work out a lot better. – Sylin Aug 06 '14 at 14:46
  • Ahh! Ok I see why you chose 1 and not 0 now in your first code sample. I didn't know that the text in an element counts as a child _node_. But then couldn't we just pick topNode among those with 0 child _element_? There also seems to be some [quirks with the elementFromPoint in Opera and Safari](http://stackoverflow.com/a/19363125/2526908) too. I'll try working this in together with the fix for IE. – Sylin Aug 07 '14 at 06:09
  • elementFromPoint doesn't return text nodes, and element nodes would generally have at least one child (which may be a text node). The code does work in Opera (just tried it) and Safari. But different versions may produce different results. Interestingly, Opera doesn't need the code – it seems to be the only browser that automatically maintains the view on resize. – Rick Hitchcock Aug 07 '14 at 14:41
  • Oh ok, by element, I meant like a HTML tag, and not counting any plain texts in it; I didn't know the word to use to distinguish them from nodes that includes both containg tags and plain texts. I haven't tested anything out on different browsers yet, but that will be my goal for tomorrow :), Opera's scroll behaviour is certainly a surprise o.o – Sylin Aug 07 '14 at 14:50
  • What version of Opera were you using? My Opera 24.0 didn't maintain the view just like other browsers. – Sylin Aug 08 '14 at 03:21
  • I'm so sorry, I was mistaken about Opera maintaining the view. The width must have been constant as I was sizing. (Just tried it now.) I've been playing around with the code at http://stackoverflow.com/questions/13597157/get-dom-text-node-from-point, and doing scrollBy based on the rectangle offset, but I'm having little luck. If there is a solution to this, I think we're close. – Rick Hitchcock Aug 08 '14 at 03:36
  • I think I've got the bit for adjusting topNode cut-off now :) I've switched the code to jQuery to help with browser compatibility, but sadly, we still have to handle the compatibility for `elementFromPoint` ourselves. I'll have a look at the question you linked as well as the other fixes we've mentioned earlier for various browsers. I'll edit on the question. – Sylin Aug 08 '14 at 08:59