Ceci est une carte interactive SVG et Vanilla Javascript

Que j'ai produite pour le site Lacroix.fr

La carte des régions de France est un fichier SVG inline.
Le javascript fait le lien entre les différentes régions et les popins à partir de leurs attributs ID.
Région sur la carte : id="NOM-REGION" => Popin correspondante : id="NOM-REGION-detail".

XML région dans la carte SVG

<path class="region" id="nouvelle-caledonie" fill="#ECEDEC" stroke="#B1AAA1" stroke-width="2" d="M502.598 1056.623l-2.735 1.31 13.959 etc."/>

La classe region est nécessaire pour le bon fonctionnement du javascript.
L'attribut "id" aussi pour faire le lien avec la popin.
On peut facilement modifier les couleurs de la carte via CSS en ciblant .map .region fill et stroke.

HTML popin région

<div class="region-detail" id="corse-detail">
    <a href="#" class="close"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="23" height="23" xml:space="preserve"><path fill="none" stroke="#B1AAA1" stroke-width="2" stroke-miterlimit="10" d="M0 0l23 23M23 0L0 23"/></svg></a>
    <a href="#" class="title">Corse</a>
    <div class="scrolling-menu">
        <ul class="menu">
            <li><a href="#"><span>Corse-du-Sud</span> (2A) ></a></li>
            <li><a href="#"><span>Haute-Corse</span> (2B) ></a></li>
        </ul>
    </div>
    <a href="#" class="region-link">> Voir tous les résultats de la région : Corse</a>
</div>

La classe region-detail est nécessaire pour le bon fonctionnement du javascript.
L'attribut "id" aussi pour faire le lien depuis la région sur la carte.

Un peu de SCSS

scss/common/_map.scss

.interactive-map est le conteneur principal.
.map est le SVG inline
.region sont les path du SVG.
    avec un attribut [data-opened=opened] quand on clique dessus.
.region-detail sont les popins.
    Au rollover, on ne montre que le titre, classe .visible.
    Au click, on montre les liens, classe .opened.
    Pour Safari qui bugue sur les mouvements, on bloque les transitions avec la classe .no-transition.
.scrolling-menu permet d'activer le scroll (système) au menu avec les liens.

.interactive-map{
    max-width: 600px;
    margin: auto;
    .map{
        position: relative;
        .region{
            cursor: pointer;
            etc.
            &[data-opened=opened]{
                fill: $colorFillOn;
                stroke: $colorFillOn;
            }
        }
        .region-detail{
            top: -20rem;
            background: $white;
            border: .2rem solid $orange;
            etc.
            &.visible{
                display: block;
            }
            &.no-transition{
                transition: none;
            }
            &.opened{
                left: 50%;
                top: 50%;
                etc.
                .scrolling-menu{
                    display: block;
                    overflow: auto;
                    max-height: 40vh;
                    @include tablet-to-large{
                        max-height: 50vh;
                    }
                }
                .menu{
                    display: block;
                    overflow: hidden;
                }
                etc.

Un peu de javascript vanilla

js/general.js

isDetailOpened est une variable booléenne qui permet d'indiquer si on a ouvert une popin.

var isDetailOpened = false;

Au mouseover, si aucune région n'a été cliquée, on positionne la popin via la fonction pozDetail() (voir plus bas) puis on la rend visible en y appliquant la classe "visible".

var regions = document.querySelectorAll('.interactive-map .map .region');
regions.forEach(function(elt){
    elt.addEventListener('mouseover', function(e){
        e.preventDefault();
        if(!isDetailOpened){
            pozDetail(e);
            var that = e.currentTarget;
            var detail = document.getElementById(that.getAttribute('id') + '-detail');
            detail.classList.add('visible');
        }
    });

Au mouseleave, si la région n'a pas été cliquée, on ferme la popin.

    elt.addEventListener('mouseleave', function(e){
        e.preventDefault();
        var that = e.currentTarget;
        if(that.getAttribute('data-opened') !== 'opened'){
            var detail = document.getElementById(that.getAttribute('id') + '-detail');
            detail.classList.remove('opened');
            detail.classList.remove('visible');
        }
    });

Au mousemove, si aucun région n'a été cliquée, on déplace la popin via la fonction pozDetail() (voir plus bas).

    elt.addEventListener('mousemove', function(e){
        e.preventDefault();
        if(!isDetailOpened){
            pozDetail(e);
        }
    });

Au click:
Si une popin est déjà ouverte, on la ferme en y enlevant les classes "visible" et "opened" ;
Si une région a déjà été cliquée, on la repasse en mode non-cliquée en passant son attribut data-opened à false ;
On passe la région en mode ouverte en passant son attribut data-opened à opened ;
On ouvre la popin en y ajoutant les classes "visible" et "opened" ;
Puis on vérifie qu'elle est bien dans le viewport, sinon on la replace dans le viewport (fonction setPoz()).

    elt.addEventListener('click', function(e){
        e.preventDefault();
        var detailOpened = document.querySelector('.interactive-map .map .region-detail.opened');
        if(detailOpened){
            detailOpened.classList.remove('visible')
            detailOpened.classList.remove('opened');
        }
        var region = document.querySelector('.interactive-map .map .region[data-opened=opened]');
        if(region){
            region.setAttribute('data-opened', 'false');
        }
        var that = e.currentTarget;
        that.setAttribute('data-opened', 'opened');
        var detail = document.getElementById(that.getAttribute('id') + '-detail');
        detail.style = '';
        detail.classList.add('visible');
        detail.classList.add('opened');
        function setPoz() {
            var t = detail.getBoundingClientRect().top;
            var b = detail.getBoundingClientRect().bottom;
            var limitT = 0;
            var limitB = windowHeight;
            if(t <= limitT){
                var delta = Math.abs(t) + limitT + 10;
                detail.style.top = 'calc(50% + ' + delta + 'px)';
            }
            else if(b > limitB){
                var delta = b - limitB + 10;
                detail.style.top = 'calc(50% - ' + delta + 'px)';
            }
        };
        if(isDesktopContext){
            var tempo = (isDetailOpened) ? 0 : (isSafari) ? 0 : 600
            setTimeout(setPoz, tempo);
        }
        isDetailOpened = true;
        document.removeEventListener('click', closeRegion);
        setTimeout(function(){
            document.addEventListener('click', closeRegion);
        }, 600);
    });
});

Le positionnement de la popin sur la souris est centralisé dans une fonction pozDetail().

function pozDetail(e){
    var that = e.currentTarget;     var detail = document.getElementById(that.getAttribute('id') + '-detail');
    var detailW = (detail.offsetWidth == 0) ? 300 : detail.offsetWidth;
    var detailH = (detail.offsetHeight == 0) ? 50 : detail.offsetHeight;
    var pox = that.getBoundingClientRect().left - that.parentElement.parentElement.getBoundingClientRect().left - (detailW/2);
    var x = e.pageX;
    var y = e.pageY + 20;
    pox = x - that.parentElement.parentElement.getBoundingClientRect().left - (detailW/2);
    console.log("pox: ", pox);
    pox = (pox < 0) ? 0 : pox;
    pox = (pox + detailW > windowWidth) ? 0 : pox;
    var poy = that.getBoundingClientRect().top - that.parentElement.parentElement.getBoundingClientRect().top;
    poy = y - (that.parentElement.parentElement.getBoundingClientRect().top + window.pageYOffset);
    poy = (that.getBoundingClientRect().top - that.parentElement.parentElement.getBoundingClientRect().top > that.parentElement.parentElement.offsetHeight - 200) ? poy - (detailH*1.5) : poy;
    detail.style.left = pox + 'px';
    detail.style.top = poy + 'px';
}

La fermeture de la popin est appliquée au document dans son ensemble sauf pour .region-detail et ses enfants (sauf le bouton close qui a sa propre fonction onclick.
Déclaration au onclick des .region (voir plus haut) : document.addEventListener('click', closeRegion);

function closeRegion(e) {
    var isInDetail = e.target.closest('.region-detail');
    if(!isInDetail){
        isDetailOpened = false;
        var detailOpened = document.querySelector('.interactive-map .map .region-detail.opened');
        if(detailOpened){
            detailOpened.classList.remove('visible');
            detailOpened.classList.remove('opened');
        }
        var region = document.querySelector('.interactive-map .map .region[data-opened=opened]');
        if(region){
            region.setAttribute('data-opened', 'false');
        }
        document.removeEventListener('click', closeRegion);
    }
}