iOS Safari - Comment désactiver le survol mais autoriser les div à défilement à défiler normalement?


100

Je travaille sur une application Web basée sur iPad et je dois éviter le défilement excessif pour que cela ressemble moins à une page Web. J'utilise actuellement ceci pour figer la fenêtre et désactiver le défilement excessif:

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

Cela fonctionne très bien pour désactiver le défilement excessif, mais mon application a plusieurs divs défilables, et le code ci-dessus les empêche de défiler .

Je cible uniquement iOS 5 et au-dessus, j'ai donc évité les solutions de piratage comme iScroll. Au lieu de cela, j'utilise ce CSS pour mes divs à défilement:

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

Cela fonctionne sans le script de survol du document, mais ne résout pas le problème de défilement div.

Sans un plugin jQuery, y a-t-il un moyen d'utiliser le correctif de dépassement mais d'exempter mes divs $ ('. Scrollable')?

ÉDITER:

J'ai trouvé quelque chose qui est une solution décente:

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

La fenêtre se déplace toujours lorsque vous faites défiler le début ou la fin du div. J'aimerais également trouver un moyen de désactiver cela.


essayé votre dernier aussi mais n'a pas fonctionné non plus
Santiago Rebella

J'ai pu empêcher la fenêtre de bouger lorsque vous faites défiler la fin du div en capturant explicitement l'événement de défilement sur le parent du div à défilement et en ne lui permettant pas de défiler réellement. Si vous utilisez jquery mobile, il est logique de le faire au niveau de la page comme ceci: $ ('div [data-role = "page"]'). On ('scroll', function (e) {e.preventDefault ();});
Christopher Johnson


J'ai trouvé ce script qui corrige ce problème! :) github.com/lazd/iNoBounce
Jan Šafránek

Pourquoi publieriez-vous à nouveau le lien si quelqu'un au-dessus de votre message l'a publié 7 mois plus tôt?
Denny

Réponses:


84

Cela résout le problème lorsque vous faites défiler le début ou la fin de la 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();
});

Notez que cela ne fonctionnera pas si vous souhaitez bloquer le défilement de la page entière lorsqu'un div n'a pas de débordement. Pour bloquer cela, utilisez le gestionnaire d'événements suivant au lieu de celui immédiatement ci-dessus (adapté de cette question ):

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

Cela ne fonctionnera pas s'il y a une iframe dans la zone de défilement et que l'utilisateur commence à faire défiler cette iframe. Existe-t-il une solution de contournement pour cela?
Timo

2
Cela a très bien fonctionné - c'est certainement mieux que de simplement cibler .scrollabledirectement (ce que j'avais essayé à l'origine pour résoudre ce problème). Si vous êtes un noob JavaScript et que vous voulez un code facile pour supprimer ces gestionnaires quelque part, ces deux lignes fonctionnent très bien pour moi! $(document).off('touchmove'); AND $('body').off('touchmove touchstart', '.scrollable');
Devin

Cela a parfaitement fonctionné pour moi. Merci beaucoup, tu m'as fait gagner des heures!
marcgg

1
Cela ne fonctionne pas s'il n'y a pas assez de contenu dans le div pour faire défiler. Quelqu'un a posé une question distincte qui a répondu à cela ici: stackoverflow.com/q/16437182/40352
Chris

Comment puis-je autoriser plusieurs classes ".scrollable"? cela fonctionne bien avec un, mais je dois aussi faire défiler un autre div. Merci!
MeV

23

L'utilisation de l'excellente réponse de Tyler Dodge était restée à la traîne sur mon iPad, j'ai donc ajouté du code d'étranglement, maintenant c'est assez fluide. Il y a parfois des sauts minimes lors du défilement.

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

De plus, l'ajout du CSS suivant corrige certains problèmes de rendu ( source ):

.scrollable {
    overflow: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
}
.scrollable * {
    -webkit-transform: translate3d(0,0,0);
}

Cela ne fonctionnera pas s'il y a une iframe dans la zone de défilement et que l'utilisateur commence à faire défiler cette iframe. Existe-t-il une solution de contournement pour cela?
Timo

1
Semble fonctionner parfaitement pour faire glisser vers l'arrière, mais glisser vers le bas déplacera toujours le safari.
Abadaba

1
Une solution géniale ... Merci beaucoup :)
Aamir Shah

Cela a fonctionné pour moi. Merci! Je passe plus de 1,5 jours pour résoudre ce problème.
Achintha Samindika

C'est génial, cela a très bien fonctionné et m'a évité un stress supplémentaire en essayant de trouver une solution. Merci Kuba!
Leonard

12

Commencez par empêcher les actions par défaut sur l'ensemble de votre document comme d'habitude:

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

Arrêtez ensuite votre classe d'éléments de se propager au niveau du document. Cela l'empêche d'atteindre la fonction ci-dessus et ainsi e.preventDefault () n'est pas lancé:

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

Ce système semble plus naturel et moins intensif que le calcul de la classe sur tous les mouvements tactiles. Utilisez .on () plutôt que .bind () pour les éléments générés dynamiquement.

Considérez également ces balises méta pour éviter que des choses malheureuses ne se produisent lors de l'utilisation de votre div à défilement:

<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" />

7

Pouvez-vous simplement ajouter un peu plus de logique à votre code de désactivation de défilement excessif pour vous assurer que l'élément ciblé en question n'est pas celui que vous souhaitez faire défiler? Quelque chose comme ça:

document.body.addEventListener('touchmove',function(e){
     if(!$(e.target).hasClass("scrollable")) {
       e.preventDefault();
     }
 });

3
Merci ... Il semble que cela devrait fonctionner, mais ce n'est pas le cas. Aussi, ne devrait-il pas être "scrollable" et non ".scrollable" (avec le point)?
Jeff

1
Il semble que ce soit l'élément le plus profondément imbriqué qui reçoit l'événement tactile, vous devrez peut-être vérifier tous vos parents pour voir si vous êtes dans une div à défilement.
Christopher Johnson

3
Pourquoi utiliser document.body.addEventListener si jQuery est utilisé? Est-ce pour une raison?
fnagel

7

La meilleure solution à cela est css / html: créez un div pour envelopper vos éléments, si vous ne l'avez pas déjà, et réglez-le sur une position fixe et un dépassement caché. Facultatif, réglez la hauteur et la largeur à 100% si vous voulez qu'il remplisse tout l'écran et rien que tout l'écran

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


5

Vérifiez si l'élément défilant fait déjà défiler vers le haut lorsque vous essayez de faire défiler vers le haut ou vers le bas lorsque vous essayez de faire défiler vers le bas, puis empêchez l'action par défaut d'arrêter le déplacement de la page entière.

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

J'ai dû vérifier e.originalEvent.touches [0] .pageY au lieu de e.originalEvent.pageY. Cela a fonctionné mais seulement si vous êtes déjà à la fin du div de défilement. Lorsque le défilement est en cours (par exemple, vous avez fait défiler très rapidement) il ne s'arrête pas une fois que la fin du div scrollable est atteinte.
Keen Sage

4

Je cherchais un moyen d'empêcher le défilement de tout le corps lorsqu'il y a une fenêtre contextuelle avec une zone de défilement (une fenêtre contextuelle "panier" qui a une vue déroulante de votre panier).

J'ai écrit une solution beaucoup plus élégante en utilisant un javascript minimal pour simplement basculer la classe "noscroll" sur votre corps lorsque vous avez un popup ou un div que vous souhaitez faire défiler (et non "faire défiler" tout le corps de la page).

tandis que les navigateurs de bureau observent un débordement: caché - iOS semble ignorer cela à moins que vous ne définissiez la position sur fixe ... ce qui fait que la page entière a une largeur étrange, vous devez donc également définir la position et la largeur manuellement. utilisez ce css:

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

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

C'est très utile et une approche minimale, exactement ce dont j'avais besoin. Réglage de la position sur fixe, avec haut: 0; gauche: 0; largeur: 100%; étaient les éléments qui me manquaient. Ceci est également utile pour les menus déroulants.
bdanin

3

J'ai travaillé un peu de contournement sans jquery. Pas parfait mais fonctionne bien (surtout si vous avez un scroll-x dans un scoll-y) https://github.com/pinadesign/overscroll/

N'hésitez pas à participer et à l'améliorer


1
J'ai eu le même problème que Jeff, essayé toutes les réponses, la vôtre a fonctionné. Je vous remercie!
Dominik Schreiber

La réponse acceptée n'a fonctionné pour moi que lorsque le div avec .scrollable avait suffisamment de contenu pour le faire déborder. S'il ne débordait pas, l'effet «rebond» existait toujours. Cependant cela fonctionne parfaitement, merci!
Adam Marshall

1

Cette solution ne vous oblige pas à mettre une classe scrollable sur toutes vos divs scrollables, elle est donc plus générale. Le défilement est autorisé sur tous les éléments qui sont, ou sont des enfants, d'éléments INPUT contenteditables et de débordement scroll ou autos.

J'utilise un sélecteur personnalisé et je cache également le résultat de la vérification de l'élément pour améliorer les performances. Pas besoin de vérifier le même élément à chaque fois. Cela peut avoir quelques problèmes comme juste écrit, mais je pensais que je partagerais.

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

1

Bien que la désactivation de tous les événements "touchmove" puisse sembler une bonne idée, dès que vous aurez besoin d'autres éléments déroulants sur la page, cela posera des problèmes. En plus de cela, si vous désactivez uniquement les événements "touchmove" sur certains éléments (par exemple, body si vous voulez que la page ne puisse pas faire défiler), dès qu'il est activé ailleurs, IOS provoquera une propagation imparable dans Chrome lorsque l'URL la barre bascule.

Bien que je ne puisse pas expliquer ce comportement, il semble que le seul moyen d'empêcher semble de définir la position du corps sur fixed. Le seul problème à faire est que vous perdrez la position du document - c'est particulièrement ennuyeux dans les modaux par exemple. Une façon de résoudre ce problème serait d'utiliser ces fonctions simples de 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);
    }
}

En utilisant cette solution, vous pouvez avoir un document fixe et tout autre élément de votre page peut déborder en utilisant du CSS simple (par exemple, overflow: scroll;). Pas besoin de cours spéciaux ou de quoi que ce soit d'autre.


0

Voici une solution compatible zepto

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

0

celui-ci fonctionne pour moi (javascript simple)

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>

0

Essayez ceci Cela fonctionnera parfaitement.

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

0

J'ai eu une chance surprenante avec simple:

body {
    height: 100vh;
}

Cela fonctionne très bien pour désactiver le défilement excessif pour les fenêtres contextuelles ou les menus et cela ne force pas les barres du navigateur à apparaître comme lorsque vous utilisez position: fixed. MAIS - vous devez enregistrer la position de défilement avant de définir une hauteur fixe et la restaurer en masquant la fenêtre contextuelle, sinon le navigateur défilera vers le haut.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.