Empêche le défilement du corps mais autorise le défilement de la superposition


446

J'ai cherché une solution de type "lightbox" qui le permet mais je n'en ai pas encore trouvé (veuillez suggérer si vous en connaissez).

Le comportement que j'essaie de recréer est similaire à ce que vous verriez sur Pinterest en cliquant sur une image. La superposition peut défiler ( comme dans toute la superposition monte comme une page en haut d'une page ) mais le corps derrière la superposition est fixe.

J'ai essayé de créer cela avec seulement CSS ( c'est-à-dire une divsuperposition au-dessus de la page entière et du corps avecoverflow: hidden ), mais cela n'empêche pas le divdéfilement.

Comment empêcher le corps / la page de défiler mais continuer à défiler à l'intérieur du conteneur plein écran?


N'est-ce pas juste un plugin de "superposition" comme des centaines là-bas en utilisant la position fixée avec le défilement de débordement?
ggzone

3
Le lien vers Pinterest n'aide pas, car son contenu se trouve derrière un mur de connexion.
2540625

4
Mise à jour 2017: même si vous vous connectez à Pinterest, vous constatez que l'effet de superposition décrit dans l'OP n'existe plus - au lieu de cela, lorsque vous cliquez sur une image, vous accédez simplement à une page ordinaire affichant une grande version de l'image.
Annabel

Réponses:


624

Théorie

En regardant l'implémentation actuelle du site pinterest (cela pourrait changer à l'avenir), lorsque vous ouvrez la superposition, une noscrollclasse est appliquée à l' bodyélément et overflow: hiddenest définie, elle bodyn'est donc plus défilable.

La superposition (créé sur la volée ou déjà à l' intérieur de la page et rendu visible par l' intermédiaire display: block, il ne fait aucune différence) a position : fixedet overflow-y: scroll, avec top, left, rightet bottomdéfinir les propriétés à 0: ce style fait de la superposition remplir toute la fenêtre.

L' divintérieur de la superposition est à la place juste à l'intérieur, position: staticpuis la barre de défilement verticale que vous voyez est liée à cet élément. En conséquence, le contenu peut défiler mais la superposition reste fixe.

Lorsque vous fermez le zoom, vous masquez la superposition (via display: none) et vous pouvez également la supprimer entièrement via javascript (ou simplement le contenu à l'intérieur, c'est à vous de l'injecter).

Comme dernière étape, vous devez également supprimer la noscrollclasse dans le body(afin que la propriété overflow revienne à sa valeur initiale)


Code

Exemple de Codepen

(cela fonctionne en changeant l' aria-hiddenattribut de la superposition afin de l'afficher et de la cacher et d'augmenter son accessibilité).

Marquage
(bouton ouvert)

<button type="button" class="open-overlay">OPEN LAYER</button>

(bouton de superposition et de fermeture)

<section class="overlay" aria-hidden="true">
  <div>
    <h2>Hello, I'm the overlayer</h2>
    ...   
    <button type="button" class="close-overlay">CLOSE LAYER</button>
  </div>
</section>

CSS

.noscroll { 
  overflow: hidden;
}

.overlay { 
   position: fixed; 
   overflow-y: scroll;
   top: 0; right: 0; bottom: 0; left: 0; }

[aria-hidden="true"]  { display: none; }
[aria-hidden="false"] { display: block; }

Javascript (vanille-JS)

var body = document.body,
    overlay = document.querySelector('.overlay'),
    overlayBtts = document.querySelectorAll('button[class$="overlay"]');

[].forEach.call(overlayBtts, function(btt) {

  btt.addEventListener('click', function() { 

     /* Detect the button class name */
     var overlayOpen = this.className === 'open-overlay';

     /* Toggle the aria-hidden state on the overlay and the 
        no-scroll class on the body */
     overlay.setAttribute('aria-hidden', !overlayOpen);
     body.classList.toggle('noscroll', overlayOpen);

     /* On some mobile browser when the overlay was previously
        opened and scrolled, if you open it again it doesn't 
        reset its scrollTop property */
     overlay.scrollTop = 0;

  }, false);

});

Enfin, voici un autre exemple dans lequel la superposition s'ouvre avec un effet de fondu par un CSS transitionappliqué à la opacitypropriété. Un padding-rightest également appliqué pour éviter une redistribution sur le texte sous-jacent lorsque la barre de défilement disparaît.

Exemple de Codepen (fondu)

CSS

.noscroll { overflow: hidden; }

@media (min-device-width: 1025px) {
    /* not strictly necessary, just an experiment for 
       this specific example and couldn't be necessary 
       at all on some browser */
    .noscroll { 
        padding-right: 15px;
    }
}

.overlay { 
     position: fixed; 
     overflow-y: scroll;
     top: 0; left: 0; right: 0; bottom: 0;
}

[aria-hidden="true"] {    
    transition: opacity 1s, z-index 0s 1s;
    width: 100vw;
    z-index: -1; 
    opacity: 0;  
}

[aria-hidden="false"] {  
    transition: opacity 1s;
    width: 100%;
    z-index: 1;  
    opacity: 1; 
}

6
Ceci est la bonne réponse. Quelqu'un devrait lui donner une coche car c'est correct.
Scoota P

64
C'est tout à fait correct, mais attention, cela ne fonctionne pas dans un safari mobile. L'arrière-plan continuera de défiler et votre superposition sera corrigée.
a10s

20
Ça fonctionne bien. Le conteneur de la fenêtre de défilement divdoit avoir un style CSS position:fixedet un débordement vertical pouvant défiler. J'ai utilisé avec succès overflow-y:auto;et pour le défilement momentum / inertie iOS, j'ai ajouté -webkit-overflow-scrolling:touch;au CSS. J'utilisé display:block;, width:100%;et height:100%;CSS pour avoir une fenêtre pleine page.
Slink

10
Désolé, je ne comprends pas. Régler le corps pour qu'il déborde: caché ne désactive pas le défilement du corps élastique sur mon iPad iOS7. J'ai donc dû ajouter dans mon js: document.body.addEventListener ('touchmove', function (event) {event.preventDefault ();}, false); qui désactive SADLY tous les éléments de défilement, aussi. Je n'ai pas trouvé de solution jusqu'à présent (sans plugins supplémentaires)
Garavani

22
Si l'utilisateur a déjà fait défiler le corps vers le bas lorsque la superposition est activée, cela fera remonter le corps vers le haut lorsque vous définissez le débordement: caché; Un moyen de contourner cela?
Björn Andersson

75

Si vous souhaitez empêcher le défilement excessif sur iOS, vous pouvez ajouter une position fixée à votre classe .noscroll

body.noscroll{
    position:fixed;
    overflow:hidden;
}

106
Avec cette solution, en raison de la position fixe, le corps défilera automatiquement vers le haut de votre contenu. Cela pourrait être dérangeant pour vos utilisateurs.
tzi

5
Un autre problème avec l'utilisation position:fixedest qu'elle redimensionnait mon corps principal. Peut-être qu'il était en conflit avec d'autres CSS, mais overflow:hiddenc'est tout ce qui était nécessaire
Dex

3
cela brise le saut de focus lorsque vous parcourez les champs de saisie
Atav32

@ Atav32 Sonne comme un problème séparé, la position et le débordement ne doivent pas affecter l'ordre des onglets.
am80l

@ am80l désolé mon commentaire précédent était super vague: l'ordre de tabulation fonctionne, mais l'écran sursaute lorsque vous parcourez les champs sur iOS Safari ( stackoverflow.com/questions/31126330/… )
Atav32

44

Ne pas utiliser overflow: hidden;sur body. Il fait automatiquement défiler tout vers le haut. Il n'y a pas non plus besoin de JavaScript. Faites usage de overflow: auto;. Cette solution fonctionne même avec Safari mobile:

Structure HTML

<div class="overlay">
    <div class="overlay-content"></div>
</div>

<div class="background-content">
    lengthy content here
</div>

Coiffant

.overlay{
    position: fixed;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    background-color: rgba(0, 0, 0, 0.8);

    .overlay-content {
        height: 100%;
        overflow: scroll;
    }
}

.background-content{
    height: 100%;
    overflow: auto;
}

Voir la démo ici et le code source ici .

Mise à jour:

Pour les personnes qui souhaitent que la barre d'espace du clavier fonctionne, la page haut / bas fonctionne: vous devez vous concentrer sur la superposition, par exemple en cliquant dessus ou manuellement JS en se concentrant dessus avant que cette partie du divne réponde au clavier. Même chose lorsque la superposition est "désactivée", car elle déplace simplement la superposition sur le côté. Sinon pour le navigateur, ce ne sont que deux divs normaux et il ne saurait pas pourquoi il devrait se concentrer sur l'un d'eux.


5
solution cool, mais j'ai besoin d'envelopper tout mon contenu dans une div, ce que je n'avais pas l'intention de faire ...
Jacob Raccuia

2
C'est une solution idéale. Si quelqu'un a des problèmes avec cela, vous devrez peut-être ajouter html, body { height: 100%; }(comme dans la démo) pour que cela fonctionne correctement.
John

4
@ user18490 la partie angulaire / matérielle n'a rien à voir avec le fait que cette solution fonctionne.
Lucia

10
Cela casse les appareils mobiles UX de plusieurs façons (par exemple: masquage de la barre d'URL, dépassement de capacité, ...), même la barre d'espace pour faire défiler les ordinateurs de bureau.
nitivement

2
C'est l'option la plus robuste, mais cela complique un peu les choses. Par exemple, PageUp et PageDown ne fonctionneront pas sur l'actualisation. tout ce qui utilise les .offset()valeurs pour le calcul est foiré, etc ...
BlackPanther

42

overscroll-behavior La propriété css permet de remplacer le comportement de défilement par défaut du navigateur pour atteindre le haut / bas du contenu.

Ajoutez simplement les styles suivants à la superposition:

.overlay {
   overscroll-behavior: contain;
   ...
}

Démo Codepen

Fonctionne actuellement dans Chrome, Firefox et IE ( caniuse )

Pour plus de détails, consultez l' article des développeurs Google .


4
J'ai parcouru tout le chemin pour vous dire que cela fonctionne sur les derniers Chrome, Mozilla et Opera. Passez un bon moment!
Wannabe JavaGeek

3
Quelqu'un devrait ajouter une prime à ce sujet. C'est la bonne solution. Aucun JavaScript n'est nécessaire. :)
joseph.l.hunsaker

9
Le problème avec cette solution est qu'elle ne fonctionne que si la superposition peut défiler. Si vous avez un modal et que tout tient à l'écran, il n'y a pas de défilement à l'intérieur - cela n'arrêtera pas le défilement du corps. Ça et ça ne marche pas du tout dans Safari :)
waterplea

1
Je me suis installé avec une combinaison de cela avec overflow-y: scroll(de votre démo Codepen). Le point soulevé par @pokrishka est valide, mais dans mon cas, ce n'est pas un problème. Dans tous les cas, je me demande si les implémentations des navigateurs vont couvrir ce détail à l'avenir. J'ai trouvé que, dans Firefox, redimensionner le navigateur pour que le modal ne s'adapte pas à l'écran, puis le redimensionner à nouveau pour qu'il fasse fonctionner cette propriété (c'est-à-dire que le défilement est contenu) même après avoir redimensionné en taille réelle - jusqu'à ce que la page soit rechargée , au moins.
Marc.2377

33

La plupart des solutions ont le problème de ne pas conserver la position de défilement, j'ai donc regardé comment Facebook le fait. En plus de définir le contenu sous-jacent, position: fixedils définissent également le haut de manière dynamique pour conserver la position de défilement:

scrollPosition = window.pageYOffset;
mainEl.style.top = -scrollPosition + 'px';

Ensuite, lorsque vous supprimez à nouveau la superposition, vous devez réinitialiser la position de défilement:

window.scrollTo(0, scrollPosition);

J'ai créé un petit exemple pour démontrer cette solution

let overlayShown = false;
let scrollPosition = 0;

document.querySelector('.toggle').addEventListener('click', function() {
  if (overlayShown) {
		showOverlay();
  } else {
    removeOverlay();
  }
  overlayShown = !overlayShown;
});

function showOverlay() {
    scrollPosition = window.pageYOffset;
  	const mainEl = document.querySelector('.main-content');
    mainEl.style.top = -scrollPosition + 'px';
  	document.body.classList.add('show-overlay');
}

function removeOverlay() {
		document.body.classList.remove('show-overlay');
  	window.scrollTo(0, scrollPosition);
    const mainEl = document.querySelector('.main-content');
    mainEl.style.top = 0;
}
.main-content {
  background-image: repeating-linear-gradient( lime, blue 103px);
  width: 100%;
  height: 200vh;
}

.show-overlay .main-content {
  position: fixed;
  left: 0;
  right: 0;
  overflow-y: scroll; /* render disabled scroll bar to keep the same width */
}

.overlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.3);
  overflow: auto;
}

.show-overlay .overlay {
  display: block;
}

.overlay-content {
  margin: 50px;
  background-image: repeating-linear-gradient( grey, grey 20px, black 20px, black 40px);
  height: 120vh;
}

.toggle {
  position: fixed;
  top: 5px;
  left: 15px;
  padding: 10px;
  background: red;
}

/* reset CSS */
body {
  margin: 0;
}
<main class="main-content"></main>

  <div class="overlay">
    <div class="overlay-content"></div>
  </div>
  
  <button class="toggle">Overlay</button>


Cela me semble être la solution la plus élégante. Je sais que Google utilise souvent les mêmes astuces pour faire défiler la page et déplacer les éléments.
forallepsilon

C'est la seule et unique solution qui a fonctionné à 100% pour moi, je souhaite avoir trouvé cette réponse plus tôt.
user0103

31

Il convient de noter que parfois, l'ajout de "débordement: caché" à la balise body ne fait pas le travail. Dans ces cas, vous devrez également ajouter la propriété à la balise html.

html, body {
    overflow: hidden;
}

Pour iOS, vous devez également définirwidth: 100%; height: 100%;
Gavin

2
Cela ne résout pas un problème également rencontré dans de nombreuses autres solutions à cette question: si la fenêtre défile n'importe où sauf en haut de la page lorsque le modal est ouvert, cela provoquera un défilement vers le haut de la page. J'ai trouvé la solution de Philipp Mitterer ici comme la meilleure option qui couvre la plupart des cas.
CLL

1
Je n'ai jamais rencontré ce problème dont vous parlez (sur le Web ou sur mobile). Pourriez-vous partager le code complet (avec DOM) qui ne fonctionne pas dans un violon ou jsbin? Dans tous les cas, je me méfie d'utiliser des solutions de contournement Javascript pour résoudre ce problème.
Francisco Hodge

7

De manière générale, si vous souhaitez qu'un parent (le corps dans ce cas) l'empêche de défiler lorsqu'un enfant (la superposition dans ce cas) défile, faites de l'enfant un frère du parent pour empêcher l'événement de défilement de se propager jusqu'à le parent. Dans le cas où le parent est le corps, cela nécessite un élément d'habillage supplémentaire:

<div id="content">
</div>
<div id="overlay">
</div>

Voir Faire défiler des contenus DIV particuliers avec la barre de défilement principale du navigateur pour voir son fonctionnement.


1
Meilleure solution, vraie pensée "prête à l'emploi", mais pas si bien formulée.
Mark

Si le corps n'est pas l'élément qui défile, les barres supérieures ne glissent pas sur mobile lorsque vous faites défiler la page.
Seph Reed

7

La réponse choisie est correcte, mais présente certaines limites:

  • Des "flings" super durs avec votre doigt défileront toujours <body>en arrière-plan
  • L'ouverture du clavier virtuel en appuyant sur un <input>dans le modal dirigera tous les futurs parchemins vers<body>

Je n'ai pas de solution pour le premier problème, mais je voulais faire la lumière sur le second. Confusément, Bootstrap avait l'habitude de documenter le problème de clavier, mais ils ont prétendu qu'il était corrigé, citant http://output.jsbin.com/cacido/quiet comme exemple de la correction.

En effet, cet exemple fonctionne très bien sur iOS avec mes tests. Cependant, sa mise à niveau vers le dernier Bootstrap (v4) le casse.

Afin de comprendre quelle était la différence entre eux, j'ai réduit un cas de test pour ne plus dépendre de Bootstrap, http://codepen.io/WestonThayer/pen/bgZxBG .

Les facteurs décisifs sont bizarres. Pour éviter le problème du clavier, il semble qu'il background-color ne soit pas défini sur la racine <div>contenant le modal et que le contenu du modal doit être imbriqué dans un autre <div>, qui peut avoir été background-colordéfini.

Pour le tester, décommentez la ligne ci-dessous dans l'exemple Codepen:

.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  display: none;
  overflow: hidden;
  -webkit-overflow-scrolling: touch;
  /* UNCOMMENT TO BREAK */
/*   background-color: white; */
}

4

Pour les appareils tactiles, essayez d'ajouter une 101vhdiv transparente de 1px de largeur et de hauteur minimale dans le wrapper de la superposition. Ajoutez ensuite -webkit-overflow-scrolling:touch; overflow-y: auto;à l'emballage. Cela incite le safari mobile à penser que la superposition peut défiler, interceptant ainsi l'événement tactile du corps.

Voici un exemple de page. Ouvert sur safari mobile: http://www.originalfunction.com/overlay.html

https://gist.github.com/YarGnawh/90e0647f21b5fa78d2f678909673507f


Jup, ça a fait l'affaire. Il suffit d'ajouter -webkit-overflow-scrolling:touch;pour chaque conteneur déroulant pour intercepter les événements tactiles.
Mati

Je suis toujours en mesure de faire défiler sur iPhone
Simone Zandara

@SeniorSimoneZandara Je ne le suis pas. cela fonctionne bien pour empêcher le défilement d'arrière-plan
godblessstrawberry

3

Le comportement que vous souhaitez empêcher est appelé chaînage de défilement. Pour le désactiver, définissez

overscroll-behavior: contain;

sur votre superposition en CSS.


3

J'ai trouvé cette question en essayant de résoudre le problème que j'avais avec ma page sur Ipad et Iphone - le corps défilait lorsque j'affichais un div fixe sous forme de popup avec image.

Certaines réponses sont bonnes, mais aucune n'a résolu mon problème. J'ai trouvé le billet de blog suivant de Christoffer Pettersson. La solution présentée ici a aidé à résoudre le problème que j'avais avec les appareils iOS et à résoudre mon problème d'arrière-plan de défilement.

Six choses que j'ai apprises sur le défilement de l'élastique sur iOS Safari

Comme il a été suggéré, j'inclus les principaux points de l'article de blog au cas où le lien deviendrait obsolète.

"Afin de désactiver le fait que l'utilisateur puisse faire défiler la page d'arrière-plan pendant que le" menu est ouvert ", il est possible de contrôler quels éléments doivent être autorisés à faire défiler ou non, en appliquant du JavaScript et une classe CSS.

Sur la base de cette réponse Stackoverflow, vous pouvez contrôler que les éléments avec la désactivation du défilement ne doivent pas effectuer leur action de défilement par défaut lorsque l'événement touchmove est déclenché. "

 document.ontouchmove = function ( event ) {

    var isTouchMoveAllowed = true, target = event.target;

    while ( target !== null ) {
        if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) {
            isTouchMoveAllowed = false;
            break;
        }
        target = target.parentNode;
    }

    if ( !isTouchMoveAllowed ) {
        event.preventDefault();
    }
};

Et puis mettez la classe disable-scrolling sur la page div:

<div class="page disable-scrolling">

2

Vous pouvez facilement le faire avec certains "nouveaux" CSS et JQuery.

Initialement: body {... overflow:auto;} Avec JQuery, vous pouvez basculer dynamiquement entre «superposition» et «corps». Lorsque sur «corps», utilisez

body {
   position: static;
   overflow: auto;
}

En utilisation "superposition"

body {
   position: sticky;
   overflow: hidden;
}

JQuery pour le commutateur ('body' -> 'overlay'):

$("body").css({"position": "sticky", "overflow": "hidden"});

JQuery pour le commutateur ('overlay' -> 'body'):

$("body").css({"position": "static", "overflow": "auto"});

1

Je voudrais ajouter aux réponses précédentes parce que j'ai essayé de le faire, et une disposition s'est cassée dès que j'ai changé le corps en position: fixe. Pour éviter cela, j'ai également dû régler la taille du corps à 100%:

function onMouseOverOverlay(over){
    document.getElementsByTagName("body")[0].style.overflowY = (over?"hidden":"scroll");
    document.getElementsByTagName("html")[0].style.position = (over?"fixed":"static");
    document.getElementsByTagName("html")[0].style.height = (over?"100%":"auto");
}

1

Utilisez le code HTML suivant:

<body>
  <div class="page">Page content here</div>
  <div class="overlay"></div>
</body>

Ensuite, JavaScript pour intercepter et arrêter le défilement:

$(".page").on("touchmove", function(event) {
  event.preventDefault()
});

Ensuite, pour ramener les choses à la normale:

$(".page").off("touchmove");


1

Si l'intention est de désactiver sur les appareils mobiles / tactiles, la façon la plus simple de le faire est d'utiliser touch-action: none;.

Exemple:

const app = document.getElementById('app');
const overlay = document.getElementById('overlay');

let body = '';

for (let index = 0; index < 500; index++) {
  body += index + '<br />';
}

app.innerHTML = body;
app.scrollTop = 200;

overlay.innerHTML = body;
* {
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

#app {
  background: #f00;
  position: absolute;
  height: 100%;
  width: 100%;
  overflow-y: scroll;
  line-height: 20px;
}

#overlay {
  background: rgba(0,0,0,.5);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  padding: 0 0 0 100px;
  overflow: scroll;
}
<div id='app'></div>
<div id='overlay'></div>

(L'exemple ne fonctionne pas dans le contexte de débordement de pile. Vous devrez le recréer dans une page autonome.)

Si vous souhaitez désactiver le défilement du #appconteneur, ajoutez simplement touch-action: none;.


1
Si l'action tactile fonctionnait réellement sur iOS, c'est. Et dans ce cas, il ne serait pas nécessaire d'envelopper l'ensemble de votre «application» dans une div séparée.
powerbuoy

0

essaye ça

var mywindow = $('body'), navbarCollap = $('.navbar-collapse');    
navbarCollap.on('show.bs.collapse', function(x) {
                mywindow.css({visibility: 'hidden'});
                $('body').attr("scroll","no").attr("style", "overflow: hidden");
            });
            navbarCollap.on('hide.bs.collapse', function(x) {
                mywindow.css({visibility: 'visible'});
                $('body').attr("scroll","yes").attr("style", "");
            });

0

Si vous souhaitez arrêter le défilement body / html, ajoutez comme suit

CSS

    html, body {
        height: 100%;
    }

    .overlay{
        position: fixed;
        top: 0px;
        left: 0px;
        right: 0px;
        bottom: 0px;
        background-color: rgba(0, 0, 0, 0.8);

        .overlay-content {
            height: 100%;
            overflow: scroll;
        }
    }

    .background-content{
        height: 100%;
        overflow: auto;
    }

HTML

    <div class="overlay">
        <div class="overlay-content"></div>
    </div>

    <div class="background-content">
        lengthy content here
    </div>

Fondamentalement, vous pourriez le faire sans JS.

L'idée principale est d'ajouter html / body avec hauteur: 100% et débordement: auto. et à l'intérieur de votre superposition, vous pouvez soit activer / désactiver le défilement en fonction de vos besoins.

J'espère que cela t'aides!



-2

Utilisez le code ci-dessous pour désactiver et activer la barre de défilement.

Scroll = (
    function(){
          var x,y;
         function hndlr(){
            window.scrollTo(x,y);
            //return;
          }  
          return {

               disable : function(x1,y1){
                    x = x1;
                    y = y1;
                   if(window.addEventListener){
                       window.addEventListener("scroll",hndlr);
                   } 
                   else{
                        window.attachEvent("onscroll", hndlr);
                   }     

               },
               enable: function(){
                      if(window.removeEventListener){
                         window.removeEventListener("scroll",hndlr);
                      }
                      else{
                        window.detachEvent("onscroll", hndlr);
                      }
               } 

          }
    })();
 //for disabled scroll bar.
Scroll.disable(0,document.body.scrollTop);
//for enabled scroll bar.
Scroll.enable();
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.