iOS Safari - Como desabilitar o overscroll, mas permitir que os divs roláveis ​​rolem normalmente?

100

Estou trabalhando em um aplicativo da web baseado em iPad e preciso evitar a rolagem excessiva para que pareça menos com uma página da web. No momento, estou usando isso para congelar a janela de visualização e desativar o deslocamento excessivo:

document.body.addEventListener('touchmove',function(e){
      e.preventDefault();
  });

Isso funciona muito bem para desativar o overscroll, mas meu aplicativo tem vários divs roláveis, e o código acima os impede de rolar .

Estou visando o iOS 5 e superior apenas, então evitei soluções hacky como o iScroll. Em vez disso, estou usando este CSS para meus divs roláveis:

.scrollable {
    -webkit-overflow-scrolling: touch;
    overflow-y:auto;
}

Isso funciona sem o script de overscroll do documento, mas não resolve o problema de rolagem div.

Sem um plugin jQuery, existe alguma maneira de usar a correção overscroll, mas isentar meus divs $ ('. Scrollable')?

EDITAR:

Eu encontrei algo que é uma solução decente:

 // Disable overscroll / viewport moving on everything but scrollable divs
 $('body').on('touchmove', function (e) {
         if (!$('.scrollable').has($(e.target)).length) e.preventDefault();
 });

A janela de visualização ainda se move quando você rola para além do início ou do final do div. Eu gostaria de encontrar uma maneira de desativar isso também.

Jeff
fonte
tentei seu último também, mas também não funcionou
Santiago Rebella
Consegui evitar que a janela de visualização se movesse quando você rolar além do final do div, capturando explicitamente o evento scroll no pai do div rolável e não permitindo que ele realmente role. Se você estiver usando jquery mobile, faz sentido fazer isso no nível da página, como: $ ('div [data-role = "page"]'). On ('scroll', function (e) {e.preventDefault ();});
Christopher Johnson
github.com/lazd/iNoBounce faz maravilhas
David Underhill,
Eu encontrei este script que corrige esse problema! :) github.com/lazd/iNoBounce
Jan Šafránek
Por que você postaria o link novamente se alguém acima de sua postagem o postou 7 meses antes?
Denny

Respostas:

84

Isso resolve o problema ao rolar para além do início ou do final do div

var selScrollable = '.scrollable';
// Uses document because document will be topmost level in bubbling
$(document).on('touchmove',function(e){
  e.preventDefault();
});
// Uses body because jQuery on events are called off of the element they are
// added to, so bubbling would not work if we used document instead.
$('body').on('touchstart', selScrollable, function(e) {
  if (e.currentTarget.scrollTop === 0) {
    e.currentTarget.scrollTop = 1;
  } else if (e.currentTarget.scrollHeight === e.currentTarget.scrollTop + e.currentTarget.offsetHeight) {
    e.currentTarget.scrollTop -= 1;
  }
});
// Stops preventDefault from being called on document if it sees a scrollable div
$('body').on('touchmove', selScrollable, function(e) {
  e.stopPropagation();
});

Observe que isso não funcionará se você quiser bloquear a rolagem da página inteira quando um div não tiver estouro. Para bloquear isso, use o seguinte manipulador de eventos em vez do imediatamente acima (adaptado desta pergunta ):

$('body').on('touchmove', selScrollable, function(e) {
    // Only block default if internal div contents are large enough to scroll
    // Warning: scrollHeight support is not universal. (https://stackoverflow.com/a/15033226/40352)
    if($(this)[0].scrollHeight > $(this).innerHeight()) {
        e.stopPropagation();
    }
});
Tyler Dodge
fonte
Isso não funcionará se houver um iframe dentro da área rolável e o usuário começar a rolar nesse iframe. Existe uma solução alternativa para isso?
Timo
2
Funcionou muito bem - isso é definitivamente melhor do que apenas direcionar .scrollablediretamente (que é o que eu tentei originalmente para resolver esse problema). Se você é um novato em JavaScript e quer um código fácil para remover esses manipuladores em algum ponto da linha, essas duas linhas funcionam muito bem para mim! $(document).off('touchmove'); AND $('body').off('touchmove touchstart', '.scrollable');
Devin
Funcionou perfeitamente para mim. Muito obrigado, você me economizou horas!
marcgg
1
Isso não funciona se não houver conteúdo suficiente no div para rolar. Alguém fez uma pergunta separada que respondeu aqui: stackoverflow.com/q/16437182/40352
Chris
Como posso permitir mais de uma classe ".scrollable"? ele funciona bem com um, mas preciso tornar outro div rolável também. Obrigado!
MeV,
23

Usar a excelente resposta de Tyler Dodge continuou demorando no meu iPad, então eu adicionei algum código de limitação, agora é bastante suave. Às vezes, há alguns pulos mínimos durante a rolagem.

// Uses document because document will be topmost level in bubbling
$(document).on('touchmove',function(e){
  e.preventDefault();
});

var scrolling = false;

// Uses body because jquery on events are called off of the element they are
// added to, so bubbling would not work if we used document instead.
$('body').on('touchstart','.scrollable',function(e) {

    // Only execute the below code once at a time
    if (!scrolling) {
        scrolling = true;   
        if (e.currentTarget.scrollTop === 0) {
          e.currentTarget.scrollTop = 1;
        } else if (e.currentTarget.scrollHeight === e.currentTarget.scrollTop + e.currentTarget.offsetHeight) {
          e.currentTarget.scrollTop -= 1;
        }
        scrolling = false;
    }
});

// Prevents preventDefault from being called on document if it sees a scrollable div
$('body').on('touchmove','.scrollable',function(e) {
  e.stopPropagation();
});

Além disso, adicionar o seguinte CSS corrige algumas falhas de renderização ( fonte ):

.scrollable {
    overflow: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
}
.scrollable * {
    -webkit-transform: translate3d(0,0,0);
}
Kuba Holuj
fonte
Isso não funcionará se houver um iframe dentro da área rolável e o usuário começar a rolar nesse iframe. Existe uma solução alternativa para isso?
Timo
1
Parece funcionar perfeitamente para arrastar para trás, mas arrastar para baixo ainda moverá o safári.
Abadaba,
1
Uma solução incrível ... Muito obrigado :)
Aamir Shah
Isso funcionou para mim. Obrigado! Gastei mais de 1,5 dias para resolver esse problema.
Achintha Samindika
Isso é incrível, funcionou muito bem e me salvou de mais estresse tentando encontrar uma solução. Obrigado Kuba!
Leonard
12

Em primeiro lugar, evite ações padrão em todo o seu documento, como de costume:

$(document).bind('touchmove', function(e){
  e.preventDefault();           
});

Em seguida, interrompa a propagação de sua classe de elementos para o nível do documento. Isso impede que ele alcance a função acima e, portanto, e.preventDefault () não é iniciado:

$('.scrollable').bind('touchmove', function(e){
  e.stopPropagation();
});

Este sistema parece ser mais natural e menos intensivo do que calcular a classe em todos os movimentos de toque. Use .on () em vez de .bind () para elementos gerados dinamicamente.

Considere também estas metatags para evitar que coisas desagradáveis ​​aconteçam ao usar seu div rolável:

<meta content='True' name='HandheldFriendly' />
<meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' name='viewport' />
<meta name="viewport" content="width=device-width" />
Jonathan Tonge
fonte
7

Você pode simplesmente adicionar um pouco mais de lógica em seu código de desativação de overscroll para garantir que o elemento em questão não seja aquele que você gostaria de rolar? Algo assim:

document.body.addEventListener('touchmove',function(e){
     if(!$(e.target).hasClass("scrollable")) {
       e.preventDefault();
     }
 });
DeveloperJoe
fonte
3
Obrigado ... Parece que isso deveria funcionar, mas não funciona. Além disso, não deveria ser "rolável" e não ".scrollable" (com o ponto)?
Jeff
1
Parece que é o elemento aninhado mais profundamente que recebe o evento de toque, portanto, talvez seja necessário verificar todos os seus pais para ver se você está em um div rolável.
Christopher Johnson
3
Por que alguém usaria document.body.addEventListener se jQuery for usado? É por algum motivo?
fnagel de
7

A melhor solução para isso é css / html: Faça uma div para envolver seus elementos, se ainda não a tiver e defina-a para a posição fixa e overflow oculto. Opcional, defina a altura e a largura como 100% se quiser preencher toda a tela e nada além da tela inteira

#wrapper{
  height: 100%;
  width: 100%;
  position: fixed;
  overflow: hidden;
}
<div id="wrapper">
  <p>All</p>
  <p>Your</p>
  <p>Elements</p>
</div>

Elias Fyksen
fonte
5

Verifique se o elemento rolável já está rolado para cima ao tentar rolar para cima ou para baixo ao tentar rolar para baixo e, a seguir, evitando que a ação padrão interrompa o movimento da página inteira.

var touchStartEvent;
$('.scrollable').on({
    touchstart: function(e) {
        touchStartEvent = e;
    },
    touchmove: function(e) {
        if ((e.originalEvent.pageY > touchStartEvent.originalEvent.pageY && this.scrollTop == 0) ||
            (e.originalEvent.pageY < touchStartEvent.originalEvent.pageY && this.scrollTop + this.offsetHeight >= this.scrollHeight))
            e.preventDefault();
    }
});
João
fonte
Tive que verificar e.originalEvent.touches [0] .pageY em vez de e.originalEvent.pageY. Funcionou, mas apenas se você já estiver no final da div de rolagem. Quando a rolagem está em andamento (por exemplo, você rolou muito rápido), ela não para quando o final da div rolável é alcançado.
Keen Sage de
4

Eu estava procurando uma maneira de evitar a rolagem de todo o corpo quando há um pop-up com uma área de rolagem (um pop-up de "carrinho de compras" que tem uma exibição de rolagem do seu carrinho).

Eu escrevi uma solução muito mais elegante usando javascript mínimo para apenas alternar a classe "noscroll" em seu corpo quando você tem um pop-up ou div que gostaria de rolar (e não "overscroll" todo o corpo da página).

enquanto navegadores de desktop observam overflow: hidden - o iOS parece ignorar isso, a menos que você defina a posição como fixa ... o que faz com que a página inteira tenha uma largura estranha, então você também deve definir a posição e a largura manualmente. use este css:

.noscroll {
    overflow: hidden;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
}

e este jquery:

/* fade in/out cart popup, add/remove .noscroll from body */
$('a.cart').click(function() {
    $('nav > ul.cart').fadeToggle(100, 'linear');
    if ($('nav > ul.cart').is(":visible")) {
        $('body').toggleClass('noscroll');
    } else {
        $('body').removeClass('noscroll');
    }
});

/* close all popup menus when you click the page... */
$('body').click(function () {
    $('nav > ul').fadeOut(100, 'linear');
    $('body').removeClass('noscroll');
});

/* ... but prevent clicks in the popup from closing the popup */
$('nav > ul').click(function(event){
    event.stopPropagation();
});
nrutas
fonte
Isso é muito útil e uma abordagem mínima, exatamente o que eu precisava. Definindo a posição para fixa, com topo: 0; esquerda: 0; largura: 100%; foram os elementos que estava faltando. Isso também é útil para menus flutuantes.
bdanin,
3

Eu trabalhei um pequeno workarround sem jquery. Não perfert, mas funciona bem (especialmente se você tiver um scroll-x em um scoll-y) https://github.com/pinadesign/overscroll/

Sinta-se à vontade para participar e melhorar

PiñaDesign
fonte
1
Tive o mesmo problema do Jeff, tentei todas as respostas, a sua funcionou. Obrigado!
Dominik Schreiber
A resposta aceita só funcionou para mim quando o div com .scrollable teve conteúdo suficiente para causar o estouro. Se não transbordou, o efeito de 'salto' ainda existia. No entanto, isso funciona perfeitamente, obrigado!
Adam Marshall
1

Esta solução não requer que você coloque uma classe rolável em todos os seus divs roláveis, então é mais geral. A rolagem é permitida em todos os elementos que são ou são filhos de INPUT elements contenteditables e overflow scroll ou autos.

Eu uso um seletor personalizado e também armazeno em cache o resultado da verificação no elemento para melhorar o desempenho. Não há necessidade de verificar o mesmo elemento todas as vezes. Isso pode ter alguns problemas, como apenas escrito, mas pensei em compartilhar.

$.expr[':'].scrollable = function(obj) {
    var $el = $(obj);
    var tagName = $el.prop("tagName");
    return (tagName !== 'BODY' && tagName !== 'HTML') && (tagName === 'INPUT' || $el.is("[contentEditable='true']") || $el.css("overflow").match(/auto|scroll/));
};
function preventBodyScroll() {
    function isScrollAllowed($target) {
        if ($target.data("isScrollAllowed") !== undefined) {
            return $target.data("isScrollAllowed");
        }
        var scrollAllowed = $target.closest(":scrollable").length > 0;
        $target.data("isScrollAllowed",scrollAllowed);
        return scrollAllowed;
    }
    $('body').bind('touchmove', function (ev) {
        if (!isScrollAllowed($(ev.target))) {
            ev.preventDefault();
        }
    });
}
jcbdrn
fonte
1

Embora desabilitar todos os eventos "touchmove" possa parecer uma boa ideia, assim que você precisar de outros elementos roláveis ​​na página, isso causará problemas. Além disso, se você desabilitar apenas eventos "touchmove" em certos elementos (por exemplo, corpo se você quiser que a página não seja rolável), assim que for habilitado em qualquer outro lugar, o IOS causará uma propagação imparável no Chrome quando o URL bar alterna.

Embora eu não possa explicar esse comportamento, parece que a única forma de prevenir parece definir a posição do corpo para fixed. O único problema é que você perderá a posição do documento - isso é especialmente irritante em modais, por exemplo. Uma maneira de resolver isso seria usar estas funções simples do VanillaJS:

function disableDocumentScrolling() {
    if (document.documentElement.style.position != 'fixed') {
        // Get the top vertical offset.
        var topVerticalOffset = (typeof window.pageYOffset != 'undefined') ?
            window.pageYOffset : (document.documentElement.scrollTop ? 
            document.documentElement.scrollTop : 0);
        // Set the document to fixed position (this is the only way around IOS' overscroll "feature").
        document.documentElement.style.position = 'fixed';
        // Set back the offset position by user negative margin on the fixed document.
        document.documentElement.style.marginTop = '-' + topVerticalOffset + 'px';
    }
}

function enableDocumentScrolling() {
    if (document.documentElement.style.position == 'fixed') {
        // Remove the fixed position on the document.
        document.documentElement.style.position = null;
        // Calculate back the original position of the non-fixed document.
        var scrollPosition = -1 * parseFloat(document.documentElement.style.marginTop);
        // Remove fixed document negative margin.
        document.documentElement.style.marginTop = null;
        // Scroll to the original position of the non-fixed document.
        window.scrollTo(0, scrollPosition);
    }
}

Usando essa solução, você pode ter um documento fixo e qualquer outro elemento em sua página pode estourar usando CSS simples (por exemplo, overflow: scroll;). Não há necessidade de aulas especiais ou qualquer outra coisa.

Nicolas Bouvrette
fonte
0

Aqui está uma solução compatível com zepto

    if (!$(e.target).hasClass('scrollable') && !$(e.target).closest('.scrollable').length > 0) {
       console.log('prevented scroll');
       e.preventDefault();
       window.scroll(0,0);
       return false;
    }
Christopher Johnson
fonte
0

este funciona para mim (JavaScript puro)

var fixScroll = function (className, border) {  // className = class of scrollElement(s), border: borderTop + borderBottom, due to offsetHeight
var reg = new RegExp(className,"i"); var off = +border + 1;
function _testClass(e) { var o = e.target; while (!reg.test(o.className)) if (!o || o==document) return false; else o = o.parentNode; return o;}
document.ontouchmove  = function(e) { var o = _testClass(e); if (o) { e.stopPropagation(); if (o.scrollTop == 0) { o.scrollTop += 1; e.preventDefault();}}}
document.ontouchstart = function(e) { var o = _testClass(e); if (o && o.scrollHeight >= o.scrollTop + o.offsetHeight - off) o.scrollTop -= off;}
}

fixScroll("fixscroll",2); // assuming I have a 1px border in my DIV

html:

<div class="fixscroll" style="border:1px gray solid">content</div>
RJS
fonte
0

Experimente isso. Funcionará perfeitamente.

$('body.overflow-hidden').delegate('#skrollr-body','touchmove',function(e){
    e.preventDefault();
    console.log('Stop skrollrbody');
}).delegate('.mfp-auto-cursor .mfp-content','touchmove',function(e){
    e.stopPropagation();
    console.log('Scroll scroll');
});
Srinivasan Thirupathi
fonte
0

Tive uma sorte surpreendente com o simples:

body {
    height: 100vh;
}

Funciona muito bem para desativar a rolagem excessiva de pop-ups ou menus e não força as barras do navegador a aparecerem como ao usar position: fixed. MAS - você precisa salvar a posição de rolagem antes de definir a altura fixa e restaurá-la ao ocultar o pop-up, caso contrário, o navegador rolará para cima.

Láďa Durchánek
fonte