Modificando location.hash sem rolagem de página

147

Temos algumas páginas usando ajax para carregar conteúdo e há algumas ocasiões em que precisamos criar um link direto para uma página. Em vez de ter um link para "Usuários" e pedir às pessoas para clicarem em "configurações", é útil poder vincular as pessoas às configurações user.aspx #

Para permitir que as pessoas nos forneçam links corretos para as seções (para suporte técnico etc.), eu o configurei para modificar automaticamente o hash no URL sempre que um botão for clicado. O único problema é que, quando isso acontece, ela também rola a página para esse elemento.

Existe uma maneira de desativar isto? Abaixo está como eu estou fazendo isso até agora.

$(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;
    });
});

Eu esperava que return false;isso impedisse a rolagem da página - mas apenas faz com que o link não funcione. Então, isso foi comentado por enquanto, para que eu possa navegar.

Alguma ideia?

CBarr
fonte

Respostas:

111

Etapa 1: você precisa desativar o ID do nó até que o hash tenha sido definido. Isso é feito removendo o ID do nó enquanto o hash está sendo definido e, em seguida, adicionando-o novamente.

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

Etapa 2: alguns navegadores acionarão a rolagem com base em onde o nó com ID foi visto pela última vez, portanto, você precisa ajudá-los um pouco. Você precisa adicionar um extra divna parte superior da janela de visualização, definir seu ID como hash e reverter tudo:

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 );
}

Etapa 3: envolva-o em um plug-in e use-o em vez de escrever para location.hash...

Borgar
fonte
1
Não, o que está acontecendo na etapa 2 é que uma div oculta é criada e colocada no local atual do pergaminho. É o mesmo visualmente que a posição: fixo / superior: 0. Assim, a barra de rolagem é "movida" para exatamente o mesmo local em que está atualmente.
Borgar
3
Essa solução realmente funciona bem - no entanto, a linha: top: $ .scroll (). Top + 'px' deve ser: top: $ (window) .scrollTop () + 'px'
Mark Perkins
27
Seria útil saber quais navegadores precisam da etapa 2 aqui.
djc
1
Obrigado Borgar. No entanto, me intriga que você esteja anexando a div fx antes de excluir o ID do nó de destino. Isso significa que há um instante em que existem IDs duplicados no documento. Parece ser um problema em potencial, ou pelo menos maneiras ruins;)
Ben
1
Muito obrigado, isso também me ajudou. Mas também notei um efeito colateral quando isso está em ação: costumo rolar bloqueando o "botão do meio do mouse" e simplesmente movendo o mouse na direção em que quero rolar. No Google Chrome, isso interrompe o fluxo de rolagem. Existe talvez uma solução alternativa sofisticada para isso?
YMMD 14/07/12
99

Use history.replaceStateou history.pushState* para alterar o hash. Isso não acionará o salto para o elemento associado.

Exemplo

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

Demo no JSFiddle

* Se você deseja obter histórico e suporte para trás

Comportamento histórico

Se você estiver usando history.pushStatee não desejar rolar a página quando o usuário usar os botões de histórico do navegador (avançar / retroceder), confira a scrollRestorationconfiguração experimental (somente Chrome 46+) .

history.scrollRestoration = 'manual';

Suporte do navegador

HaNdTriX
fonte
5
replaceStateé provavelmente o melhor caminho a percorrer aqui. A diferença é pushStateadicionar um item ao seu histórico, enquanto replaceStateisso não acontece.
Aakil Fernandes 11/02
Obrigado @AakilFernandes, você está certo. Acabei de atualizar a resposta.
HaNdTriX
Nem sempre é o bodyque rola, às vezes é o documentElement. Veja esta essência por Diego Perini
Matijs 10/03/2015
1
alguma idéia sobre ie8 / 9 aqui?
user151496
1
@ user151496 check-out: stackoverflow.com/revisions/…
HaNdTriX
90

Acho que encontrei uma solução bastante simples. O problema é que o hash no URL também é um elemento na página na qual você é rolado. se eu apenas acrescentar algum texto ao hash, agora ele não fará mais referência a um elemento existente!

$(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;
    });
});

Agora, o URL aparece como page.aspx#btn_elementIDnão sendo um ID real na página. Acabei de remover "btn_" e obtenho o ID do elemento real

CBarr
fonte
5
Ótima solução. Mais indolor do lote.
Swader
Note que se você já está comprometido com Page.aspx # elementID URLs por algum motivo você pode reverter esta técnica e prepend "btn_" a todos os seus IDs
joshuahedlund
2
Não funciona se você estiver usando: pseudo-seletores de destino em CSS.
Jason T Featheringham
1
Adoro esta solução! Estamos usando o hash do URL para a navegação baseada em AJAX, acrescentando os IDs com um /parece lógico.
Connell
3

Um trecho do seu código original:

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

Mude para:

$("#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")
});
Daniel
fonte
Isso não funcionará porque a configuração da hashpropriedade fará com que a página role de qualquer maneira.
Jordanbtucker 15/09/14
3

Recentemente, eu estava construindo um carrossel que conta com window.location.hasha manutenção do estado e descobri que os navegadores Chrome e Webkit forçariam a rolagem (mesmo para um destino não visível) com um empurrão estranho quando o window.onhashchangeevento for disparado.

Mesmo tentando registrar um manipulador que interrompe a propagação:

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

Não fez nada para interromper o comportamento padrão do navegador. A solução que encontrei foi usar window.history.pushStatepara alterar o hash sem disparar os efeitos colaterais indesejáveis.

 $("#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;
});

Você pode ouvir o evento normal de troca de hash e my.hashchange:

$(window).on('hashchange my.hashchange', function(e, newUrl, oldUrl){
  // @todo - do something awesome!
});
max
fonte
my.hashchangeé claro que é apenas um exemplo de nome de evento
max
2

Ok, este é um tópico bastante antigo, mas eu pensei em entrar em contato porque a resposta 'correta' não funciona bem com CSS.

Essa solução basicamente impede que o evento click mova a página para que possamos obter a posição de rolagem primeiro. Em seguida, adicionamos manualmente o hash e o navegador aciona automaticamente um evento de alteração de hash . Capturamos o evento hashchange e voltamos para a posição correta. Um retorno de chamada separa e evita que seu código cause um atraso, mantendo o hash hacking em um só lugar.

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!
});
Egor Kloos
fonte
Você não precisa do hashchange, apenas rolagem de volta imediatamente
daniel.gindi
2

Erm, eu tenho um método um tanto grosseiro, mas definitivamente funcional.
Apenas armazene a posição atual de rolagem em uma variável temp e, em seguida, redefina-a após alterar o hash. :)

Então, para o exemplo original:

$("#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);
});
Jan Paepke
fonte
O problema com este método é com navegadores móveis você pode notar que a rolagem é modificado
Tofandel
1

Eu não acho que isso seja possível. Até onde eu sei, a única vez em que um navegador não rola para uma alteração document.location.hashé se o hash não existir na página.

Este artigo não está diretamente relacionado à sua pergunta, mas discute o comportamento típico do navegador de alterardocument.location.hash

Ben S
fonte
1

se você usar o evento hashchange com o analisador de hash, poderá impedir a ação padrão nos links e alterar location.hash adicionando um caractere para ter diferença na propriedade id de um elemento

$('a[href^=#]').on('click', function(e){
    e.preventDefault();
    location.hash = $(this).attr('href')+'/';
});

$(window).on('hashchange', function(){
    var a = /^#?chapter(\d+)-section(\d+)\/?$/i.exec(location.hash);
});
polkila
fonte
Perfeito! Depois de cinco horas tentando resolver o problema com a recarga de hash ...: / Esta foi a única coisa que funcionou !! Obrigado!!!
Igor Trindade
1

Adicionando isso aqui, porque as perguntas mais relevantes foram todas marcadas como duplicatas, apontando aqui…

Minha situação é mais simples:

  • o usuário clica no link ( a[href='#something'])
  • o manipulador de cliques: e.preventDefault()
  • função smoothscroll: $("html,body").stop(true,true).animate({ "scrollTop": linkoffset.top }, scrollspeed, "swing" );
  • então window.location = link;

Dessa forma, a rolagem ocorre e não há salto quando a localização é atualizada.

ptim
fonte
0

A outra maneira de fazer isso é adicionar uma div que está oculta na parte superior da janela de exibição. Essa div recebe o ID do hash antes que o hash seja adicionado ao URL .... para que você não obtenha um pergaminho.

Mike Rifgin
fonte
0

Aqui está minha solução para guias habilitadas para histórico:

    var tabContainer = $(".tabs"),
        tabsContent = tabContainer.find(".tabsection").hide(),
        tabNav = $(".tab-nav"), tabs = tabNav.find("a").on("click", function (e) {
                e.preventDefault();
                var href = this.href.split("#")[1]; //mydiv
                var target = "#" + href; //#myDiv
                tabs.each(function() {
                    $(this)[0].className = ""; //reset class names
                });
                tabsContent.hide();
                $(this).addClass("active");
                var $target = $(target).show();
                if ($target.length === 0) {
                    console.log("Could not find associated tab content for " + target);
                } 
                $target.removeAttr("id");
                // TODO: You could add smooth scroll to element
                document.location.hash = target;
                $target.attr("id", href);
                return false;
            });

E para mostrar a última guia selecionada:

var currentHashURL = document.location.hash;
        if (currentHashURL != "") { //a tab was set in hash earlier
            // show selected
            $(currentHashURL).show();
        }
        else { //default to show first tab
            tabsContent.first().show();
        }
        // Now set the tab to active
        tabs.filter("[href*='" + currentHashURL + "']").addClass("active");

Observe o *=na filterchamada. Isso é específico da jQuery e, sem ela, as guias ativadas para o histórico falharão.

user1429980
fonte
0

Esta solução cria uma div no scrollTop real e a remove após alterar o hash:

$('#menu a').on('click',function(){
    //your anchor event here
    var href = $(this).attr('href');
    window.location.hash = href;
    if(window.location.hash == href)return false;           
    var $jumpTo = $('body').find(href);
    $('body').append(
        $('<div>')
            .attr('id',$jumpTo.attr('id'))
            .addClass('fakeDivForHash')
            .data('realElementForHash',$jumpTo.removeAttr('id'))
            .css({'position':'absolute','top':$(window).scrollTop()})
    );
    window.location.hash = href;    
});
$(window).on('hashchange', function(){
    var $fakeDiv = $('.fakeDivForHash');
    if(!$fakeDiv.length)return true;
    $fakeDiv.data('realElementForHash').attr('id',$fakeDiv.attr('id'));
    $fakeDiv.remove();
});

opcional, acionando o evento âncora no carregamento da página:

$('#menu a[href='+window.location.hash+']').click();

fonte
0

Eu tenho um método mais simples que funciona para mim. Basicamente, lembre-se do que realmente é o hash em HTML. É um link de âncora para uma tag Name. É por isso que rola ... o navegador está tentando rolar para um link de ancoragem. Então, dê um!

  1. Logo abaixo da tag BODY, coloque sua versão:
 <a name="home"> </a> <a name="firstsection"> </a> <a name="secondsection"> </a> <a name="thirdsection"> </a>
  1. Nomeie sua seção divs com classes em vez de IDs.

  2. No seu código de processamento, retire a marca de hash e substitua por um ponto:

    var trimPanel = loadhash.substring (1); // perde o hash

    var dotSelect = '.' + trimPanel; // substitui o hash pelo ponto

    $ (dotSelect) .addClass ("painel ativo"). show (); // mostra a div associada ao hash.

Por fim, remova element.preventDefault ou return: false e permita que a navegação aconteça. A janela permanecerá na parte superior, o hash será anexado ao URL da barra de endereço e o painel correto será aberto.

ColdSharper
fonte
0

Eu acho que você precisa redefinir a rolagem para sua posição antes da alteração de 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).addClass('selected').attr("href").replace("javascript:","");
        eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function() {
            var scrollLocation = $(window).scrollTop();
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash = $(this).attr("id");
            $(window).scrollTop( scrollLocation );
    });
});
iProDev
fonte
0

Se em sua página você usa o id como uma espécie de ponto de ancoragem e tem cenários em que deseja que os usuários anexem #something ao final do URL e faça a rolagem da página para a seção #something usando seu próprio conjunto animado definido função javascript, o ouvinte de evento hashchange não poderá fazer isso.

Se você simplesmente colocar um depurador imediatamente após o evento hashchange, por exemplo, algo como isto (bem, eu uso o jquery, mas você entendeu):

$(window).on('hashchange', function(){debugger});

Você notará que, assim que você altera o URL e pressiona o botão Enter, a página para na seção correspondente imediatamente; somente depois disso, sua própria função de rolagem definida será acionada e meio que rola para a seção, que parece muito mal.

Minha sugestão é:

  1. não use id como ponto de ancoragem para a seção para a qual você deseja rolar.

  2. Se você deve usar o ID, como eu. Em vez disso, use o ouvinte de evento 'popstate', ele não irá rolar automaticamente até a seção que você anexa ao URL; em vez disso, você pode chamar sua própria função definida dentro do evento popstate.

    $(window).on('popstate', function(){myscrollfunction()});

Finalmente, você precisa fazer um pequeno truque em sua própria função de rolagem definida:

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

exclua o ID da sua tag e redefina-o.

Isso deve fazer o truque.

Shuwei
fonte
-5

Adicione este código apenas ao jQuery em documentos prontos

Ref: http://css-tricks.com/snippets/jquery/smooth-scrolling/

$(function() {
  $('a[href*=#]:not([href=#])').click(function() {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html,body').animate({
          scrollTop: target.offset().top
        }, 1000);
        return false;
      }
    }
  });
});
Saygın Karahan
fonte