iOS Safari - jak wyłączyć nadmierne przewijanie, ale pozwolić przewijanym elementom div na normalne przewijanie?

100

Pracuję nad aplikacją internetową opartą na iPadzie i muszę zapobiegać nadmiernemu przewijaniu, aby nie wyglądała jak strona internetowa. Obecnie używam tego do zamrożenia widocznego obszaru i wyłączenia nadmiernego przewijania:

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

Działa to świetnie, aby wyłączyć nadmierne przewijanie, ale moja aplikacja ma kilka przewijalnych elementów div, a powyższy kod zapobiega ich przewijaniu .

Celuję tylko w system iOS 5 i nowszy, więc uniknąłem hakerskich rozwiązań, takich jak iScroll. Zamiast tego używam tego CSS dla moich przewijalnych elementów div:

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

Działa to bez skryptu przewijania dokumentu, ale nie rozwiązuje problemu przewijania div.

Czy bez wtyczki jQuery można zastosować poprawkę overscroll, ale zwolnić z niej moje elementy div $ („. Scrollable”)?

EDYTOWAĆ:

Znalazłem coś, co jest przyzwoitym rozwiązaniem:

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

Okno robocze nadal się przesuwa, gdy przewijasz początek lub koniec elementu div. Chciałbym również znaleźć sposób, aby to wyłączyć.

Jeff
źródło
wypróbowałeś również swój ostatni, ale też nie zadziałał
Santiago Rebella
Udało mi się zapobiec przesuwaniu się widocznego obszaru, gdy przewijasz poza koniec elementu div, wyraźnie przechwytując zdarzenie przewijania w nadrzędnym przewijanym elemencie div i uniemożliwiając mu przewijanie. Jeśli używasz jQuery mobile, warto to zrobić na poziomie strony: $ ('div [data-role = "page"]'). On ('scroll', function (e) {e.preventDefault ();});
Christopher Johnson,
github.com/lazd/iNoBounce działa cuda
David Underhill
Znalazłem ten skrypt, który rozwiązuje ten problem! :) github.com/lazd/iNoBounce
Jan Šafránek
Dlaczego miałbyś publikować link ponownie, jeśli ktoś nad Twoim postem opublikował go 7 miesięcy wcześniej?
Denny

Odpowiedzi:

84

To rozwiązuje problem, gdy przewijasz poza początek lub koniec 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();
});

Pamiętaj, że to nie zadziała, jeśli chcesz zablokować przewijanie całej strony, gdy element div nie jest przepełniony. Aby to zablokować, użyj następującej procedury obsługi zdarzeń zamiast tej bezpośrednio powyżej (dostosowanej z tego pytania ):

$('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
źródło
To nie zadziała, jeśli w przewijalnym obszarze znajduje się element iframe, a użytkownik zacznie przewijać ten element iframe. Czy istnieje obejście tego problemu?
Timo
2
Zadziałało świetnie - jest to zdecydowanie lepsze niż zwykłe kierowanie .scrollablebezpośrednio (co pierwotnie próbowałem rozwiązać ten problem). Jeśli jesteś noobem JavaScript i chcesz, aby prosty kod usuwał te programy obsługi gdzieś w dół, te dwie linie działają świetnie! $(document).off('touchmove'); AND $('body').off('touchmove touchstart', '.scrollable');
Devin,
U mnie zadziałało idealnie. Wielkie dzięki, zaoszczędziłeś mi godziny!
marcgg
1
To nie działa, jeśli w elemencie div nie ma wystarczającej ilości treści do przewijania. Ktoś zadał osobne pytanie, na które odpowiedział, że tutaj: stackoverflow.com/q/16437182/40352
Chris
Jak mogę zezwolić na więcej niż jedną klasę „.scrollable”? działa dobrze z jednym, ale potrzebuję również przewijania innego elementu div. Dzięki!
MeV
23

Korzystanie z doskonałej odpowiedzi Tylera Dodge'a wciąż pozostawało w tyle na moim iPadzie, więc dodałem kod ograniczający przepustowość, teraz jest całkiem płynny. Czasami podczas przewijania występuje minimalne pomijanie.

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

Ponadto dodanie następującego kodu CSS naprawia niektóre błędy renderowania ( źródło ):

.scrollable {
    overflow: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
}
.scrollable * {
    -webkit-transform: translate3d(0,0,0);
}
Kuba Holuj
źródło
To nie zadziała, jeśli w przewijalnym obszarze znajduje się element iframe, a użytkownik zacznie przewijać ten element iframe. Czy istnieje obejście tego problemu?
Timo
1
Wydaje się, że działa idealnie do przeciągania do tyłu, ale przeciąganie w dół nadal będzie przenosić safari.
Abadaba,
1
Niesamowite rozwiązanie ... Wielkie dzięki :)
Aamir Shah
To zadziałało dla mnie. Dzięki! Na rozwiązanie tego problemu poświęcam ponad 1,5 dnia.
Achintha Samindika
To jest niesamowite, zadziałało świetnie i zaoszczędziło mi dodatkowego stresu, próbując znaleźć rozwiązanie. Dziękuję Kuba!
Leonard,
12

Najpierw jak zwykle zapobiegaj domyślnym działaniom na całym dokumencie:

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

Następnie zatrzymaj propagację klasy elementów na poziom dokumentu. Zapobiega to osiągnięciu powyższej funkcji i dlatego e.preventDefault () nie jest inicjowana:

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

Ten system wydaje się być bardziej naturalny i mniej intensywny niż obliczanie klasy dla wszystkich ruchów dotykowych. Użyj .on () zamiast .bind () dla dynamicznie generowanych elementów.

Weź również pod uwagę te metatagi, aby zapobiec niefortunnym zdarzeniom podczas korzystania z przewijalnego elementu div:

<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
źródło
7

Czy możesz dodać trochę więcej logiki do kodu wyłączającego przewijanie, aby upewnić się, że dany element docelowy nie jest tym, który chcesz przewinąć? Coś takiego:

document.body.addEventListener('touchmove',function(e){
     if(!$(e.target).hasClass("scrollable")) {
       e.preventDefault();
     }
 });
DeweloperJoe
źródło
3
Dzięki ... Wygląda na to, że to powinno działać, ale tak nie jest. Czy też nie powinno być „przewijalne”, a nie „.scrollable” (z kropką)?
Jeff
1
Wygląda na to, że jest to najbardziej zagnieżdżony element, który odbiera zdarzenie dotykowe, więc może być konieczne sprawdzenie wszystkich rodziców, aby zobaczyć, czy znajdujesz się w przewijalnym div.
Christopher Johnson,
3
Po co używać document.body.addEventListener, jeśli używany jest jQuery? Czy to ma jakiś powód?
fnagel
7

Najlepszym rozwiązaniem jest css / html: utwórz element div, w którym zawiniesz swoje elementy, jeśli jeszcze go nie masz, ustaw go na stałą i ukrytą przepełnienie. Opcjonalnie ustaw wysokość i szerokość na 100%, jeśli chcesz, aby wypełniały cały ekran i tylko cały ekran

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

Elias Fyksen
źródło
5

Sprawdź, czy przewijalny element jest już przewijany do góry podczas próby przewijania w górę lub do dołu podczas próby przewinięcia w dół, a następnie zapobiega domyślnej akcji zatrzymania całej strony.

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();
    }
});
Johan
źródło
Musiałem sprawdzić e.originalEvent.touches [0] .pageY zamiast e.originalEvent.pageY. Zadziałało, ale tylko wtedy, gdy jesteś już na końcu przewijanego div. Gdy przewijanie jest w toku (np. Przewijasz bardzo szybko), nie zatrzymuje się po osiągnięciu końca przewijalnego div.
Keen Sage
4

Szukałem sposobu, aby zapobiec przewijaniu całej treści, gdy pojawia się wyskakujące okienko z przewijalnym obszarem (wyskakujące okienko „koszyka zakupów” z przewijanym widokiem koszyka).

Napisałem o wiele bardziej eleganckie rozwiązanie, używając minimalnego javascript, aby po prostu przełączać klasę „noscroll” w Twoim ciele, gdy masz wyskakujące okienko lub element div, który chcesz przewinąć (a nie „przewinąć” całą treść strony).

podczas gdy przeglądarki stacjonarne obserwują overflow: hidden - iOS wydaje się to ignorować, chyba że ustawisz pozycję na stałą ... co powoduje, że cała strona ma dziwną szerokość, więc musisz również ręcznie ustawić pozycję i szerokość. użyj tego css:

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

i to 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
źródło
Jest to bardzo pomocne i minimalne podejście, właśnie to, czego potrzebowałem. Ustawienie pozycji na stałe, z górą: 0; po lewej: 0; szerokość: 100%; były elementami, których mi brakowało. Jest to również przydatne w przypadku menu wysuwanych.
bdanin
3

Mam trochę obejścia bez jQuery. Niezbyt perfekcyjne, ale działa dobrze (zwłaszcza jeśli masz scroll-x w scoll-y) https://github.com/pinadesign/overscroll/

Zapraszam do udziału i ulepszania

PiñaDesign
źródło
1
Miałem ten sam problem co Jeff, wypróbowałem każdą odpowiedź, twoja zadziałała. Dziękuję Ci!
Dominik Schreiber
Zaakceptowana odpowiedź zadziałała dla mnie tylko wtedy, gdy element div z .scrollable miał wystarczającą zawartość, aby spowodować przepełnienie. Jeśli się nie przepełniło, efekt „odbicia” nadal istniał. Jednak działa to doskonale, dzięki!
Adam Marshall
1

To rozwiązanie nie wymaga umieszczania przewijalnej klasy na wszystkich przewijalnych elementach DIV, więc jest bardziej ogólne. Przewijanie jest dozwolone na wszystkich elementach, które są lub są elementami podrzędnymi elementów INPUT contenteditables oraz overflow scroll lub autos.

Używam niestandardowego selektora, a także buforuję wynik sprawdzenia w elemencie, aby poprawić wydajność. Nie ma potrzeby za każdym razem sprawdzać tego samego elementu. Może to mieć kilka problemów, które zostały tylko napisane, ale pomyślałem, że podzielę się.

$.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
źródło
1

Chociaż wyłączenie wszystkich zdarzeń „touchmove” może wydawać się dobrym pomysłem, gdy tylko będziesz potrzebować innych przewijalnych elementów na stronie, spowoduje to problemy. Ponadto, jeśli wyłączysz zdarzenia „touchmove” tylko dla niektórych elementów (np. Body, jeśli chcesz, aby strona nie była przewijalna), gdy tylko zostanie włączona w innym miejscu, IOS spowoduje niepowstrzymaną propagację w Chrome, gdy adres URL pasek przełącza.

Chociaż nie potrafię wyjaśnić tego zachowania, wydaje się, że jedynym sposobem zapobiegania wydaje się ustawienie pozycji ciała na fixed. Jedynym problemem jest utrata pozycji dokumentu - jest to szczególnie denerwujące na przykład w modach. Jednym ze sposobów rozwiązania tego problemu byłoby użycie tych prostych funkcji 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);
    }
}

Korzystając z tego rozwiązania, możesz mieć stały dokument, a każdy inny element na Twojej stronie może się przepełnić za pomocą prostego CSS (np overflow: scroll;.). Nie potrzeba żadnych specjalnych zajęć ani niczego innego.

Nicolas Bouvrette
źródło
0

Oto rozwiązanie kompatybilne z Zepto

    if (!$(e.target).hasClass('scrollable') && !$(e.target).closest('.scrollable').length > 0) {
       console.log('prevented scroll');
       e.preventDefault();
       window.scroll(0,0);
       return false;
    }
Christophera Johnsona
źródło
0

ten działa dla mnie (zwykły javascript)

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
źródło
0

Spróbuj tego. Będzie działać idealnie.

$('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
źródło
0

Miałem zaskakujące szczęście z prostym:

body {
    height: 100vh;
}

Świetnie działa wyłączanie nadmiernego przewijania dla wyskakujących okienek lub menu i nie wymusza na paskach przeglądarki, aby wyglądały tak, jak podczas korzystania z pozycji: naprawiono. ALE - musisz zapisać pozycję przewijania przed ustawieniem stałej wysokości i przywrócić ją podczas ukrywania wyskakującego okienka, w przeciwnym razie przeglądarka przewinie się do góry.

Láďa Durchánek
źródło