scrollIntoView rola longe demais

107

Eu tenho uma página onde uma barra de rolagem contendo linhas de tabela com divs é gerada dinamicamente a partir do banco de dados. Cada linha da tabela age como um link, como você veria em uma lista de reprodução do YouTube ao lado do player de vídeo.

Quando um usuário visita a página, a opção em que ele está deve ir para o topo da div de rolagem. Esta funcionalidade está funcionando. O problema é que isso vai longe demais. Por exemplo, a opção em que estão ativados é cerca de 10 px muito alta. Assim, a página é visitada, a url é usada para identificar qual opção foi selecionada e então rola essa opção para o topo da div de rolagem. Nota: Esta não é a barra de rolagem da janela, é um div com uma barra de rolagem.

Estou usando este código para mover a opção selecionada para o topo do div:

var pathArray = window.location.pathname.split( '/' );

var el = document.getElementById(pathArray[5]);

el.scrollIntoView(true);

Ele o move para o topo do div, mas cerca de 10 pixels acima. Alguém sabe como consertar isso?

Matthew Wilson
fonte
44
A falta de configuração de deslocamento para scrollIntoViewé preocupante.
mystrdat

Respostas:

57

Se for cerca de 10 px, então eu acho que você poderia simplesmente ajustar manualmente o divdeslocamento de rolagem de contendo assim:

el.scrollIntoView(true);
document.getElementById("containingDiv").scrollTop -= 10;
Lucas Trzesniewski
fonte
1
Isso terá o mesmo efeito animado que scrollIntoView?
1252748
1
@thomas AFAIK, a scrollIntoViewfunção DOM simples não causa animação. Você está falando sobre o plugin scrollIntoView jQuery?
Lucas Trzesniewski
1
Isso funcionou cara, exceto que era a direção errada então tudo que eu tive que fazer foi mudar de + = 10 para - = 10 e agora está carregando da maneira certa, muito obrigado pela ajuda !!!!
Matthew Wilson
Sim. Eu estava confuso. Achei animado. Desculpe!
1252748
17
Pode animar, basta adicionar el.scrollIntoView({ behavior: 'smooth' });. O suporte não é muito bom!
daveaspinall
108

Role suavemente para uma posição adequada

Obtenha a y coordenada correta e usewindow.scrollTo({top: y, behavior: 'smooth'})

const id = 'profilePhoto';
const yOffset = -10; 
const element = document.getElementById(id);
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;

window.scrollTo({top: y, behavior: 'smooth'});
Arseniy-II
fonte
2
Esta é a resposta mais correta - podemos encontrar facilmente a posição do pergaminho jQuery('#el').offset().tope usarwindow.scrollTo
Alexander Goncharov
1
1 para esta solução. Isso me ajudou a rolar para o elemento corretamente quando eu tinha um contêiner posicionado em relação a um deslocamento superior.
Liga de
Quem vier aqui usando HashLink com React Router, é isso que funcionou para mim. No suporte de rolagem em seu HashLink, scroll={el => { const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset; const yOffset = -80; window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' }); }}- onde o deslocamento é 10-15 pixels a mais do que o tamanho de seu cabeçalho apenas para melhor espaçamento
Rob B
86

Você pode fazer isso em duas etapas:

el.scrollIntoView(true);
window.scrollBy(0, -10); // Adjust scrolling with a negative value here
fred727
fonte
23
Se a scrollIntoView()rolagem não tiver terminado antes da scrollBy()execução, a última rolagem cancelará a anterior. Pelo menos no Chrome 71.
Dave Hughes
1
Nesse caso, use o setTimeout conforme visto na resposta abaixo: stackoverflow.com/a/51935129/1856258 .. Para mim funciona sem Timeout. Obrigado!
leole de
Existe uma maneira de injetar diretamente um corretor de pixel em scrollIntoView para fazer o aplicativo mover diretamente para o local relevante? Tipo scrollIntoView({adjustment:y}), eu acho que deveria ser possível com um código customizado no final
Webwoman de
78

Posicione a âncora pelo método absoluto

Outra maneira de fazer isso é posicionar suas âncoras exatamente onde você deseja na página, em vez de depender da rolagem por deslocamento. Acho que permite um melhor controle para cada elemento (por exemplo, se você quiser um deslocamento diferente para certos elementos) e também pode ser mais resistente a alterações / diferenças de API do navegador.

<div id="title-element" style="position: relative;">
  <div id="anchor-name" style="position: absolute; top: -100px; left: 0"></div>
</div>

Agora, o deslocamento é especificado como -100px em relação ao elemento. Crie uma função para criar esta âncora para reutilização de código ou, se estiver usando uma estrutura JS moderna, como React, faça isso criando um componente que renderiza sua âncora e passe o nome da âncora e o alinhamento de cada elemento, que pode ou pode não ser o mesmo.

Depois é só usar:

const element = document.getElementById('anchor-name')
element.scrollIntoView({ behavior: 'smooth', block: 'start' });

Para uma rolagem suave com um deslocamento de 100 px.

Steve Banton
fonte
12
Esta é de longe a melhor maneira de implementar isso de maneira simples e fácil.
catch22
2
O melhor e mais nativo método, eu acho.
Даниил Пронин
1
Isso é tão simples e inteligente. Muitos dos outros apenas adicionam JS desnecessários.
RedSands
2
Isso é especialmente útil quando um link do tipo "topo da página" está abaixo de uma barra de navegação e você deseja mostrar a navegação, mas não quer nenhum outro link deslocado
Joe Moon
você pode conseguir o mesmo usando margem negativa superior, evitando envolver o elemento junto com outro com posição relativa
Sergi
15

Resolvi esse problema usando,

element.scrollIntoView({ behavior: 'smooth', block: 'center' });

Isso faz com que o elemento apareça centerapós a rolagem, então não preciso calcular yOffset.

Espero que ajude...

Imtiaz Shakil Siddique
fonte
1
Você deve usar scrollTonão em, elementmas emwindow
Arseniy-II
@ Arseniy-II Obrigado por apontar isso !!. Eu perdi essa parte!
Imtiaz Shakil Siddique
o bloqueio ajuda para cima?
Ashok kumar Ganesan
11

Isso funciona para mim no Chrome (com rolagem suave e sem hacks de tempo)

Ele apenas move o elemento, inicia a rolagem e o move de volta.

Não há "popping" visível se o elemento já estiver na tela.

pos = targetEle.style.position;
top = targetEle.style.top;
targetEle.style.position = 'relative';
targetEle.style.top = '-20px';
targetEle.scrollIntoView({behavior: 'smooth', block: 'start'});
targetEle.style.top = top;
targetEle.style.position = pos;
Ryan como
fonte
Esta é a melhor solução ... Queria ter sido marcada como resposta. Teria economizado algumas horas.
Shivam de
7

Com base em uma resposta anterior, estou fazendo isso em um projeto Angular5.

Começou com:

// el.scrollIntoView(true);
el.scrollIntoView({
   behavior: 'smooth',
   block: 'start'
});
window.scrollBy(0, -10); 

Mas isso deu alguns problemas e precisava setTimeout para scrollBy () como este:

//window.scrollBy(0,-10);
setTimeout(() => {
  window.scrollBy(0,-10)
  }, 500);

E funciona perfeitamente no MSIE11 e no Chrome 68+. Não testei no FF. 500ms era o menor atraso que eu arriscaria. Ir mais baixo às vezes falhava porque a rolagem suave ainda não havia sido concluída. Ajuste conforme necessário para seu próprio projeto.

+1 para Fred727 por esta solução simples, mas eficaz.

PrimaryW
fonte
6
Parece um pouco complicado rolar suavemente para um elemento e, em seguida, rolar um pouco para cima.
catch22
6

Supondo que você deseja rolar para os divs que estão todos no mesmo nível no DOM e têm o nome de classe "scroll-with-offset", então este CSS resolverá o problema:

.scroll-with-offset {    
  padding-top: 100px;
  margin-bottom: -100px;
}

O deslocamento do topo da página é 100px. Só funcionará como pretendido com o bloco: 'start':

element.scrollIntoView({ behavior: 'smooth', block: 'start' });

O que está acontecendo é que o ponto superior dos divs está no local normal, mas seu conteúdo interno começa 100px abaixo do local normal. É para isso que serve o padding-top: 100px. margin-bottom: -100px é para compensar a margem extra do div abaixo. Para tornar a solução completa, também adicione este CSS para compensar as margens / preenchimentos dos divs mais altos e mais baixos:

.top-div {
  padding-top: 0;
}
.bottom-div {
  margin-bottom: 0;
}
Georgy Petukhov
fonte
6

Esta solução pertence a @ Arseniy-II , acabei de simplificá-la em uma função.

function _scrollTo(selector, yOffset = 0){
  const el = document.querySelector(selector);
  const y = el.getBoundingClientRect().top + window.pageYOffset + yOffset;

  window.scrollTo({top: y, behavior: 'smooth'});
}

Uso (você pode abrir o console aqui mesmo no StackOverflow e testá-lo):

_scrollTo('#question-header', 0);
Diego Fortes
fonte
4

Você também pode usar as element.scrollIntoView() opções

el.scrollIntoView(
  { 
    behavior: 'smooth', 
    block: 'start' 
  },
);

que a maioria dos navegadores suporta

Nicholas Murray
fonte
33
No entanto, isso não suporta nenhuma opção de deslocamento.
adriendenat de
3

Eu tenho isso e funciona perfeitamente para mim:

// add a smooth scroll to element
scroll(el) {
el.scrollIntoView({
  behavior: 'smooth',
  block: 'start'});

setTimeout(() => {
window.scrollBy(0, -40);
}, 500);}

Espero que ajude.

Fernando brandão
fonte
2

Outra solução é usar "offsetTop", assim:

var elementPosition = document.getElementById('id').offsetTop;

window.scrollTo({
  top: elementPosition - 10, //add your necessary value
  behavior: "smooth"  //Smooth transition to roll
});
Kaio Dutra
fonte
2

Então, talvez isso seja um pouco desajeitado, mas até agora tudo bem. Estou trabalhando no angular 9.

Arquivo .ts

scroll(el: HTMLElement) {
  el.scrollIntoView({ block: 'start',  behavior: 'smooth' });   
}

Arquivo .html

<button (click)="scroll(target)"></button>
<div  #target style="margin-top:-50px;padding-top: 50px;" ></div>

Eu ajusto o deslocamento com margem e tampo de enchimento.

Saludos!

Ignacio Basti
fonte
1

Encontrou uma solução alternativa. Digamos que você queira rolar para um div, Elemento aqui, por exemplo, e deseja ter um espaçamento de 20px acima dele. Defina o ref como um div criado acima dele:

<div ref={yourRef} style={{position: 'relative', bottom: 20}}/> <Element />

Isso criará o espaçamento que você deseja.

Se você tiver um cabeçalho, crie um div vazio também atrás do cabeçalho e atribua a ele uma altura igual à altura do cabeçalho e faça referência a ele.

Ibrahim Al Masri
fonte
0

Minha idéia principal é criar um tempDiv acima da vista que nós queremos para ir até. Funcionou bem sem atrasos no meu projeto.

scrollToView = (element, offset) => {
    var rect = element.getBoundingClientRect();
    var targetY = rect.y + window.scrollY - offset;

    var tempDiv;
    tempDiv = document.getElementById("tempDiv");
    if (tempDiv) {
        tempDiv.style.top = targetY + "px";
    } else {
        tempDiv = document.createElement('div');
        tempDiv.id = "tempDiv";
        tempDiv.style.background = "#F00";
        tempDiv.style.width = "10px";
        tempDiv.style.height = "10px";
        tempDiv.style.position = "absolute";
        tempDiv.style.top = targetY + "px";
        document.body.appendChild(tempDiv);
    }

    tempDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

Exemplo usando

onContactUsClick = () => {
    this.scrollToView(document.getElementById("contact-us"), 48);
}

Espero que ajude

Phan Van Linh
fonte
0

Com base na resposta de Arseniy-II: Eu tinha o Caso de Uso em que a entidade de rolagem não era a própria janela, mas um Template interno (neste caso, um div). Neste cenário, precisamos definir um ID para o contêiner de rolagem e acessá-lo getElementByIdpara usar sua função de rolagem:

<div class="scroll-container" id="app-content">
  ...
</div>
const yOffsetForScroll = -100
const y = document.getElementById(this.idToScroll).getBoundingClientRect().top;
const main = document.getElementById('app-content');
main.scrollTo({
    top: y + main.scrollTop + yOffsetForScroll,
    behavior: 'smooth'
  });

Deixando aqui para o caso de alguém enfrentar situação semelhante!

kounex
fonte
0

Aqui estão meus 2 centavos.

Também tive o problema de scrollIntoView rolar um pouco além do elemento, então criei um script (javascript nativo) que precede um elemento para o destino, posicionei-o um pouco no topo com css e rolei até aquele. Depois de rolar, removo os elementos criados novamente.

HTML:

//anchor tag that appears multiple times on the page
<a href="#" class="anchors__link js-anchor" data-target="schedule">
    <div class="anchors__text">
        Scroll to the schedule
    </div>
</a>

//The node we want to scroll to, somewhere on the page
<div id="schedule">
    //html
</div>

Arquivo Javascript:

(() => {
    'use strict';

    const anchors = document.querySelectorAll('.js-anchor');

    //if there are no anchors found, don't run the script
    if (!anchors || anchors.length <= 0) return;

    anchors.forEach(anchor => {
        //get the target from the data attribute
        const target = anchor.dataset.target;

        //search for the destination element to scroll to
        const destination = document.querySelector(`#${target}`);
        //if the destination element does not exist, don't run the rest of the code
        if (!destination) return;

        anchor.addEventListener('click', (e) => {
            e.preventDefault();
            //create a new element and add the `anchors__generated` class to it
            const generatedAnchor = document.createElement('div');
            generatedAnchor.classList.add('anchors__generated');

            //get the first child of the destination element, insert the generated element before it. (so the scrollIntoView function scrolls to the top of the element instead of the bottom)
            const firstChild = destination.firstChild;
            destination.insertBefore(generatedAnchor, firstChild);

            //finally fire the scrollIntoView function and make it animate "smoothly"
            generatedAnchor.scrollIntoView({
                behavior: "smooth",
                block: "start",
                inline: "start"
            });

            //remove the generated element after 1ms. We need the timeout so the scrollIntoView function has something to scroll to.
            setTimeout(() => {
                destination.removeChild(generatedAnchor);
            }, 1);
        })
    })
})();

CSS:

.anchors__generated {
    position: relative;
    top: -100px;
}

Espero que isso ajude alguém!

rubeus
fonte
0

Eu adiciono essas dicas de css para aqueles que não resolveram esse problema com as soluções acima:

#myDiv::before {
  display: block;
  content: " ";
  margin-top: -90px; // adjust this with your header height
  height: 90px; // adjust this with your header height
  visibility: hidden;
}
Jérémy MARQUER
fonte
0

UPD: criei um pacote npm que funciona melhor do que a solução a seguir e mais fácil de usar.

Minha função smoothScroll

Peguei a solução maravilhosa de Steve Banton e escrevi uma função que a torna mais conveniente de usar. Seria mais fácil de usar window.scroll()ou até mesmo window.scrollBy(), como já tentei antes, mas esses dois têm alguns problemas:

  • Tudo fica viciado depois de usá-los com um comportamento suave.
  • Você não pode evitá-los de qualquer maneira e tem que esperar até o fim do livro. Espero que minha função seja útil para você. Além disso, há um polyfill leve que o faz funcionar no Safari e até no IE.

Aqui está o código

Basta copiá-lo e bagunçar como quiser.

import smoothscroll from 'smoothscroll-polyfill';

smoothscroll.polyfill();

const prepareSmoothScroll = linkEl => {
  const EXTRA_OFFSET = 0;

  const destinationEl = document.getElementById(linkEl.dataset.smoothScrollTo);
  const blockOption = linkEl.dataset.smoothScrollBlock || 'start';

  if ((blockOption === 'start' || blockOption === 'end') && EXTRA_OFFSET) {
    const anchorEl = document.createElement('div');

    destinationEl.setAttribute('style', 'position: relative;');
    anchorEl.setAttribute('style', `position: absolute; top: -${EXTRA_OFFSET}px; left: 0;`);

    destinationEl.appendChild(anchorEl);

    linkEl.addEventListener('click', () => {
      anchorEl.scrollIntoView({
        block: blockOption,
        behavior: 'smooth',
      });
    });
  }

  if (blockOption === 'center' || !EXTRA_OFFSET) {
    linkEl.addEventListener('click', () => {
      destinationEl.scrollIntoView({
        block: blockOption,
        behavior: 'smooth',
      });
    });
  }
};

export const activateSmoothScroll = () => {
  const linkEls = [...document.querySelectorAll('[data-smooth-scroll-to]')];

  linkEls.forEach(linkEl => prepareSmoothScroll(linkEl));
};

Para fazer um elemento de link, basta adicionar o seguinte atributo de dados:

data-smooth-scroll-to="element-id"

Além disso, você pode definir outro atributo como um complemento

data-smooth-scroll-block="center"

Representa a blockopção da scrollIntoView()função. Por padrão, é start. Leia mais sobre MDN .

Finalmente

Ajuste a função smoothScroll às suas necessidades.

Por exemplo, se você tem algum cabeçalho fixo (ou eu o chamo com a palavra masthead), pode fazer algo assim:

const mastheadEl = document.querySelector(someMastheadSelector);

// and add it's height to the EXTRA_OFFSET variable

const EXTRA_OFFSET = mastheadEl.offsetHeight - 3;

Se você não tiver esse caso, basta excluí-lo, por que não :-D.

Dmitry Zakharov
fonte
0

Para um elemento em uma linha de tabela, use JQuery para obter a linha acima da que você deseja e simplesmente role até essa linha.

Suponha que eu tenha várias linhas em uma tabela, algumas das quais devem ser revisadas por e pelo administrador. Cada linha que requer revisão tem uma seta para cima e para baixo para levá-lo ao item anterior ou seguinte para revisão.

Aqui está um exemplo completo que deve ser executado se você criar um novo documento HTML no bloco de notas e salvá-lo. Há um código extra para detectar a parte superior e inferior de nossos itens para revisão, para que não ocorram erros.

<html>
<head>
    <title>Scrolling Into View</title>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <style>
        div.scroll { height: 6em; width: 20em; overflow: auto; }
        thead th   { position: sticky; top: -1px; background: #fff; }
        .up, .down { cursor: pointer; }
        .up:hover, .down:hover { color: blue; text-decoration:underline; }
    </style>
</head>
<body>
<div class='scroll'>
<table border='1'>
    <thead>
        <tr>
            <th>Review</th>
            <th>Data</th>
        </tr>
    </thead>
    <tbody>
        <tr id='row_1'>
            <th></th>
            <td>Row 1 (OK)</td>
        </tr>
        <tr id='row_2'>
            <th></th>
            <td>Row 2 (OK)</td>
        </tr>
        <tr id='row_3'>
            <th id='jump_1'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 3 (REVIEW)</td>
        </tr>
        <tr id='row_4'>
            <th></th>
            <td>Row 4 (OK)</td>
        </tr>
        <tr id='row_5'>
            <th id='jump_2'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 5 (REVIEW)</td>
        </tr>
        <tr id='row_6'>
            <th></th>
            <td>Row 6 (OK)</td>
        </tr>
        <tr id='row_7'>
            <th></th>
            <td>Row 7 (OK)</td>
        </tr>
        <tr id='row_8'>
            <th id='jump_3'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 8 (REVIEW)</td>
        </tr>
        <tr id='row_9'>
            <th></th>
            <td>Row 9 (OK)</td>
        </tr>
        <tr id='row_10'>
            <th></th>
            <td>Row 10 (OK)</td>
        </tr>
    </tbody>
</table>
</div>
<script>
$(document).ready( function() {
    $('.up').on('click', function() {
        var id = parseInt($(this).parent().attr('id').split('_')[1]);
        if (id>1) {
            var row_id = $('#jump_' + (id - 1)).parent().attr('id').split('_')[1];
            document.getElementById('row_' + (row_id-1)).scrollIntoView({behavior: 'smooth', block: 'start'});
        } else {
            alert('At first');
        }
    });

    $('.down').on('click', function() {
        var id = parseInt($(this).parent().attr('id').split('_')[1]);
        if ($('#jump_' + (id + 1)).length) {
            var row_id = $('#jump_' + (id + 1)).parent().attr('id').split('_')[1];
            document.getElementById('row_' + (row_id-1)).scrollIntoView({behavior: 'smooth', block: 'start'});
        } else {
            alert('At last');
        }
    });
});
</script>
</body>
</html>
Martin Francis
fonte
-1

Solução simples para rolar um elemento específico para baixo

const element = document.getElementById("element-with-scroll");
element.scrollTop = element.scrollHeight - 10;
Huri
fonte