修改location.hash时不滚动页面

170
我们有几个页面使用ajax来加载内容,有一些情况下我们需要深度链接到一个页面。为了不让人们点击“设置”,而是直接链接到user.aspx#settings,这样更加方便。
为了让人们提供正确的部分链接(例如技术支持等),我设置了自动修改URL哈希值的功能,每当按钮被点击时就会触发。唯一的问题是,这样做会将页面滚动到该元素。
是否有方法可以禁用此功能?以下是我目前的操作方式。
$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash=$(this).attr("id")
            //return false;
    });
});

我原本希望return false;可以停止页面滚动,但它只会使链接根本不起作用。所以现在我将其注释掉,以便进行导航。

有什么想法吗?

20个回答

122

使用 history.replaceStatehistory.pushState* 来改变hash值。这不会触发跳转到相关元素。

例子

$(document).on('click', 'a[href^=#]', function(event) {
  event.preventDefault();
  history.pushState({}, '', this.href);
});

JSFiddle上的演示

* 如果您想要历史记录的前进和后退支持

历史行为

如果您正在使用history.pushState,并且不希望用户在浏览器的历史按钮(向前/向后)上使用时出现页面滚动,请查看实验性的scrollRestoration设置(仅限Chrome 46+)

history.scrollRestoration = 'manual';

浏览器支持情况


8
在这里,replaceState可能是更好的选择。不同之处在于,pushState会向您的历史记录中添加一个项目,而replaceState则不会。 - A F
1
这里有关于IE8/9的任何想法吗? - user151496
如果您希望后退/前进按钮按照用户的期望工作,那么pushState是正确的选择。 - max
@user151496 IE 8/9 不支持 pushState。http://caniuse.com/#search=pushState - allicarn
1
@user151496请查看:http://stackoverflow.com/revisions/3883f48a-7abd-4bc2-ad53-bfb37410a25a/view-source - HaNdTriX
显示剩余4条评论

111

步骤1:您需要解除节点ID的引爆,直到哈希设置完成。这是通过在设置哈希值时将ID从节点上移除,然后再添加回去来完成的。

hash = hash.replace( /^#/, '' );
var node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
}
document.location.hash = hash;
if ( node.length ) {
  node.attr( 'id', hash );
}

第二步:某些浏览器会根据最后一次看到有ID的节点来触发滚动,因此您需要稍微帮助它们。您需要在视口顶部添加一个额外的div,将其ID设置为哈希值,然后将所有内容滚回:

hash = hash.replace( /^#/, '' );
var fx, node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
  fx = $( '<div></div>' )
          .css({
              position:'absolute',
              visibility:'hidden',
              top: $(document).scrollTop() + 'px'
          })
          .attr( 'id', hash )
          .appendTo( document.body );
}
document.location.hash = hash;
if ( node.length ) {
  fx.remove();
  node.attr( 'id', hash );
}

第三步:将其包装在插件中,并使用该插件,而不是写入location.hash...


1
不,第二步发生的是创建了一个隐藏的div,并将其放置在滚动条当前位置。它在视觉上与position:fixed/top:0相同。因此,滚动条被“移动”到与当前位置完全相同的位置。 - Borgar
3
这个解决方案的确效果很好,不过代码中这行:top: $.scroll().top + 'px'应该改为:top: $(window).scrollTop() + 'px' - Mark Perkins
27
请问需要翻译的内容是:“It would be useful to know which browsers need step 2 here.” - djc
1
谢谢Borgar。但是让我困惑的是,在删除目标节点的ID之前,您正在附加fx div。这意味着文档中存在重复的ID的瞬间。看起来像是一个潜在的问题,或者至少是不好的做法 ;) - Ben
1
非常感谢,这对我也有所帮助。但是我还注意到了一个副作用:当它在运行时,我经常通过锁定“中间鼠标按钮”并简单地将鼠标移动到我想要滚动的方向来滚动。在Google Chrome中,这会中断滚动流程。也许有什么花哨的解决方法吗? - YMMD
显示剩余6条评论

93

我认为我可能找到了一个相当简单的解决方案。问题在于URL中的散列符号(hash)也是页面上你要滚动到的元素。如果我只是在散列符号前面添加一些文本,那么它就不再引用现有元素了!

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash.replace("btn_","")).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash="btn_"+$(this).attr("id")
            //return false;
    });
});

现在的URL看起来是page.aspx#btn_elementID,但这不是页面上真正的ID。我只需要去掉"btn_"就能得到实际的元素ID。


5
很棒的解决方案。最为轻松无痛的选择。 - Swader
请注意,如果您已经因某种原因承诺使用page.aspx#elementID URL,则可以反转此技术并在所有ID前加上“btn_”。 - joshuahedlund
4
如果你在CSS中使用了:target伪类选择器,这个功能就无法使用。 - Jason
1
喜欢这个解决方案!我们正在使用URL哈希进行基于AJAX的导航,将ID前缀添加“/”似乎很合理。 - Connell

4

最近我在构建一个轮播(carousel)时,依赖于window.location.hash来维护状态,并发现Chrome和Webkit浏览器会在触发window.onhashchange事件时强制滚动(即使到不可见的目标位置),并且存在奇怪的突然滚动。

甚至尝试注册一个停止传播的处理程序:

$(window).on("hashchange", function(e) { 
  e.stopPropogation(); 
  e.preventDefault(); 
});

我没有采取任何措施来阻止默认浏览器行为。我找到的解决方案是使用 window.history.pushState 来更改哈希值,而不会触发不良副作用。
 $("#buttons li a").click(function(){
    var $self, id, oldUrl;

    $self = $(this);
    id = $self.attr('id');

    $self.siblings().removeClass('selected'); // Don't re-query the DOM!
    $self.addClass('selected');

    if (window.history.pushState) {
      oldUrl = window.location.toString(); 
      // Update the address bar 
      window.history.pushState({}, '', '#' + id);
      // Trigger a custom event which mimics hashchange
      $(window).trigger('my.hashchange', [window.location.toString(), oldUrl]);
    } else {
      // Fallback for the poors browsers which do not have pushState
      window.location.hash = id;
    }

    // prevents the default action of clicking on a link.
    return false;
});

接着,您可以同时监听正常的hashchange事件和my.hashchange事件:

$(window).on('hashchange my.hashchange', function(e, newUrl, oldUrl){
  // @todo - do something awesome!
});

当然,my.hashchange只是一个示例事件名称。 - max

2
您的原始代码片段:

一个您的原始代码片段:

$("#buttons li a").click(function(){
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});

将此更改为:

$("#buttons li a").click(function(e){
    // need to pass in "e", which is the actual click event
    e.preventDefault();
    // the preventDefault() function ... prevents the default action.
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});

这样做不起作用,因为设置“hash”属性会导致页面滚动。 - jordanbtucker

2

我把这篇文章添加在这里,因为更相关的问题都被标记为重复,指向这里...

我的情况比较简单:

  • 用户点击链接 (a[href='#something'])
  • 点击处理程序执行: e.preventDefault()
  • 平滑滚动功能:$("html,body").stop(true,true).animate({ "scrollTop": linkoffset.top }, scrollspeed, "swing" );
  • 然后 window.location = link;

这样,当位置更新时就不会出现跳跃,而且滚动也会发生。


2

好的,这是一个相当古老的话题,但是我想提出一些建议,因为“正确”的答案在CSS方面不起作用。

这个解决方案主要防止点击事件移动页面,以便我们可以先获取滚动位置。然后手动添加哈希值,浏览器自动触发hashchange事件。我们捕获hashchange事件并滚动回正确的位置。通过回调函数,将你的哈希处理放在一个地方,避免你的代码导致延迟。

var hashThis = function( $elem, callback ){
    var scrollLocation;
    $( $elem ).on( "click", function( event ){
        event.preventDefault();
        scrollLocation = $( window ).scrollTop();
        window.location.hash = $( event.target ).attr('href').substr(1);
    });
    $( window ).on( "hashchange", function( event ){
        $( window ).scrollTop( scrollLocation );
        if( typeof callback === "function" ){
            callback();
        }
    });
}
hashThis( $( ".myAnchor" ), function(){
    // do something useful!
});

你不需要使用hashchange,直接立即滚动回去即可。 - daniel.gindi

2

我有一个比较简单但绝对可行的方法。
只需将当前滚动位置存储在临时变量中,然后在改变哈希之后重置它即可。:)

因此,对于原始示例:

$("#buttons li a").click(function(){
        $("#buttons li a").removeClass('selected');
        $(this).addClass('selected');

        var scrollPos = $(document).scrollTop();
        document.location.hash=$(this).attr("id")
        $(document).scrollTop(scrollPos);
});

这种方法的问题在于移动浏览器上,你会注意到滚动条被修改了。 - Tofandel

1

使用replaceState对我有效:

$('a[href^="#"]').click(function(){
    history.replaceState({}, '', location.toString().replace(/#.*$/, '') + $(this).attr('href'));
});

这对我有用,谢谢! - Zernel

1
  1. 在更改 URL 片段之前保存滚动位置。
  2. 更改 URL 片段。
  3. 恢复旧的滚动位置。
let oldScrollPosition = window.scrollY;
window.location.hash = addressFragment;
window.scrollTo(0, oldScrollPosition);

它很快,所以客户端不会注意到任何问题。


1
我已经实现了这个功能,而且它运行良好。不知道为什么我自己没有想到它。 - Lewis Morris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接