WickyNilliams / headroom.js

Give your pages some headroom. Hide your header until you need it
https://wicky.nillia.ms/headroom.js/
MIT License
10.86k stars 824 forks source link

Prevent pinning on anchor jumps? #38

Closed youngbrioche closed 5 years ago

youngbrioche commented 10 years ago

Is there any way to prevent pinning on anchor jumps to a higher part of the page, so anchors don't get obstructed by a pinned header?

mistic100 commented 10 years ago

If I understand correctly, add a class to you anchors and use

.anchor {
  position:relative;
  top:-100px;
}
youngbrioche commented 10 years ago

That'd work for block elements but not for anchor links within text(paragraphs). Surely there're hacks like adding padding and negative margin at the same time, but wouldn't it be nicer if headroom could just unpin the header if someone jumps to an anchor that's above the current y position?

hnrch02 commented 10 years ago

This would have been easy if @WickyNilliams would have chosen events, which give you the option to cancel them, over callbacks (see the discussion in #31). Although, I'm not really sure if callbacks are already implemented and if they are whether they allow for prevention of the pinning or not.

WickyNilliams commented 10 years ago

Could you explain how events would have helped here? I haven't pushed the callback code, so I could be swayed if there is a compelling use-case.

hnrch02 commented 10 years ago

Well if you would fire an event for pinning and unpinning one could listen for a hashchange event and then preventDefault the headroom event once.

WickyNilliams commented 10 years ago

I can't find any information on how I would catch the call to preventDefault so that pinning or unpinning could be cancelled by a listener. Do you have any resources on this? Or could you provide a code sample?

One approach with callbacks might be to listen to hash change as you stated, and then call scrollTo(0,currentScrollY-heightOfHeader) to adjust for the height of the header.

hnrch02 commented 10 years ago

You can find a little demo here: http://jsbin.com/OkejIkEt/1/edit The essential part is the return value of EventTarget.dispatchEvent which will be false if the event has been canceled from within anywhere in the bubbling phase.

WickyNilliams commented 10 years ago

That makes sense, taught me something new :)

But callbacks don't preclude offering the same type of functionality. e.g. return false from callback if you wish to prevent pin/unpin

hnrch02 commented 10 years ago

Yeah sure, I just feel that events seem more appropriate for a browser environment. Also, you can listen for an event as often as you wish whereas one can only register a single callback for pinning and unpinning, respectively.

WickyNilliams commented 10 years ago

I've pushed v0.4.0, and I decided to stick with callbacks approach. In the next version I'll allow pin/unpin to be cancelled by returning a flag, which will give @mrreynolds the ability to achieve what he wishes.

@hnrch02 I do agree, and I prefer the eventing paradigm in most cases but state is more neatly encapsulated with callbacks (it would complicate destruction of the widget if events were used).

Nettsentrisk commented 10 years ago

Is this still in the works? Can't see how I can use the callbacks to achieve this.

WickyNilliams commented 10 years ago

Apologies, been super busy recently...

I'm not even sure if callbacks are needed. As I mentioned above, can listening to the hashchange event give you what you need?

This rough code seems to work on the headroom site where this problem is also exhibited e.g. jumping to a specific version in the changelog obscures some of the text. It hasn't been thoroughly tested

window.addEventListener("hashchange", function(e) {
   var hash = location.hash.replace(/\./g, "\\."); //clean up hash 
   var anchor = document.querySelector(hash);
   window.scrollTo(0, window.pageYOffset - anchor.offsetHeight);
}, false);

This adjusts the scroll for the height of the link, ensuring it's visible. Though it is simpler if we just adjust for the height of the header as we can cache everything up front:

var header = document.querySelector("header");
var headerHeight = header.offsetHeight;

window.addEventListener("hashchange", function(e) {
   window.scrollTo(0, window.pageYOffset - headerHeight);
}, false);
acristinziani commented 10 years ago

~~Even if the problem of the TO might have been solved by the last comment, I wanted to ask, whether the feature mentioned in https://github.com/WickyNilliams/headroom.js/issues/38#issuecomment-32048463 that would "allow pin/unpin to be cancelled by returning a flag" has already been added? Can't find it in the docs, unfortunately.~~ (BTW, thanks a lot for the work on headroom!)

Update: In the meantime I solved my issue pretty roughly: I just added a further options class used to freeze the state of the header (pinned/unpinned). The code changes were minimal: adding a new options class and evaluate it in the shouldPin and shouldUnpin functions. What is then left to do is to add this class when the header should not pin/unpin.

  Headroom.options = {
    tolerance : 0,
    offset: 0,
    classes : {
      pinned : 'headroom--pinned',
      unpinned : 'headroom--unpinned',
      top: 'headroom--top',
      notTop: 'headroom--not-top',
      initial : 'headroom',
      isFrozen : 'frozen'
    }
  };
    shouldUnpin : function (currentScrollY, toleranceExceeded) {
      var scrollingDown = currentScrollY > this.lastKnownScrollY,
        pastOffset = currentScrollY >= this.offset,
        isFrozen = this.elem.classList.contains(this.classes.isFrozen);

      return scrollingDown && pastOffset && toleranceExceeded && !isFrozen;
    },

    shouldPin : function (currentScrollY, toleranceExceeded) {
      var scrollingUp  = currentScrollY < this.lastKnownScrollY,
        pastOffset = currentScrollY <= this.offset,
        isFrozen = this.elem.classList.contains(this.classes.isFrozen);

      return !isFrozen && ((scrollingUp && toleranceExceeded) || pastOffset);
    },

For sure this is not a good solution, but in case somebody needs something similar and stumbles over this, it might be useful.

Defmoves commented 10 years ago

Thanks @acristinziani your amend works a treat, also thanks to @WickyNilliams for this excellent widget.

youngbrioche commented 9 years ago

Any chance on getting @acristinziani's solution merged?

EDIT: or the one @WickyNilliams posted? Seems less complicated.

markmarijnissen commented 9 years ago

A simple but effective hack:

$(window).on('hashchange',function(){
        setTimeout(function(){
            headroom.unpin();
        },0);
    });

I am using setTimeout to schedule the cancellation immediatly after the scroll and headroom is triggered.

CathyMacars commented 8 years ago

I would benefit from this as well. I tried @markmarijnissen's trick, but it didn't work.

edit: nevermind, I forgot to convert it to vanilla js... it's working now :p

joeyhoer commented 7 years ago

While this doesn't solve the issue at hand (i.e. "anchor jumps to a higher part of the page"), I wanted to drop this here for the weary traveler.

The vanilla js version of @markmarijnissen's trick:

document.addEventListener('hashchange', function(){
  setTimeout(function(){
    headroom.unpin();
  },0);
});

This seems to only work if you increase the timeout… I suspect a race condition with the scroll events & the debouncer (which uses requestAnimationFrame).

var header = document.querySelector("header");
var headerHeight = header.offsetHeight;

window.addEventListener("hashchange", function(e) {
   window.scrollTo(0, window.pageYOffset - headerHeight);
}, false);

This solution works, but only when moving up the page, and only when clicking on "new" anchors. If you click on an anchor and are jumped/scrolled up the page, and then you scroll down and click the same anchor again, the hashchange event is not fired. You would need to add some logic to support anchor links that scroll the page down and anchor links that are equal to the current hash.


To hide the header when loading a page with a hash use:

// Unpin header on URL fragments
if(window.location.hash) {
  header.classList.add("headroom--unpinned");
}
WickyNilliams commented 5 years ago

As advised here, I would consider using the standardised scroll-padding css property. This has pretty good browser support (which will only improve over time), is built exactly for this purpose, and avoids awkward JS workarounds.

For that reason, I'm going to close. It only took 5 years to get to this point haha

konovalov-nk commented 4 years ago

I would consider using the standardised scroll-padding css property. This has pretty good browser support

Unfortunately, scroll-padding doesn't work in Safari browser without some tricks. On my recent project, I've tried to make it work without success, and in the end I've had to resort to some custom plugin like vue-scrollto.

davequested commented 2 years ago

Many thanks for the library, it's fantastic.

I'm struggling to get scroll-padding-top to work. It works if you have manually set it in the tag BEFORE headroom is loaded. But if you want to work out the height of the header first then set that to the scroll-padding-top in the browser ignores it. I'm guessing the browser needs to jump down to the right place in the page early on and doesn't get the updated value.

Anyone manage to get it working where it's dynamically setting the scroll-padding-top to the height of the header?

Thanks.