mercredi 13 janvier 2016

Implémenter une popover avec Angular Material

Pour le besoin d'un projet, je souhaitais utiliser une popover.
Malheureusement, Angular Material (1.0.1) ne propose pas encore cette directive qui est assez demandée (voir https://github.com/angular/material/issues/1217).

Je vous propose donc de créer un popover simple.
Pour cela, nous regardons sur Internet si quelqu'un a déjà créé un popover en CSS.
J'ai trouvé ce code assez simple : http://codepen.io/derekpcollins/pen/JCLhG.

Voici le code CSS une fois compilé :
.popover {
    background-color: rgba(0, 0, 0, 0.85);
    border-radius: 5px;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
    color: #fff;
    visibility: hidden;
    font-size: 12px;
    font-family: 'Helvetica',sans-serif;
    padding: 7px 10px;
    position: absolute;
    z-index: 4;
}

.popover:before {
    border-top: 7px solid rgba(0, 0, 0, 0.85);
    border-right: 7px solid transparent;
    border-left: 7px solid transparent;
    bottom: -7px;
    content: '';
    display: block;
    left: 50%;
    margin-left: -7px;
    position: absolute;
}

.popover-hover {
    visibility: visible;
}

@-webkit-keyframes fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
@-moz-keyframes fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
@-ms-keyframes fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
@-webkit-keyframes move-up {
    from {
        bottom: 30px;
    }
    to {
        bottom: 42px;
    }
}
@-moz-keyframes move-up {
    from {
        bottom: 30px;
    }
    to {
        bottom: 42px;
    }
}
@-ms-keyframes move-up {
    from {
        bottom: 30px;
    }
    to {
        bottom: 42px;
    }
}
@-webkit-keyframes fade-in {
    from   { opacity: 0; }
    to { opacity: 1; }
}
@-moz-keyframes fade-in {
    from   { opacity: 0; }
    to { opacity: 1; }
}
@-ms-keyframes fade-in {
    from   { opacity: 0; }
    to { opacity: 1; }
}
@-webkit-keyframes move-up {
    from   { bottom: 30px; }
    to { bottom: 42px; }
}
@-moz-keyframes move-up {
    from   { bottom: 30px; }
    to { bottom: 42px; }
}
@-ms-keyframes move-up {
    from   { bottom: 30px; }
    to { bottom: 42px; }
}

A présent, il nous faut créer la directive Angular JS.
Notre but, avoir un <md-popover>Bonjour !</md-popover> qui peut contenir du code HTML.

Pour cela, la directive se met à l'intérieur de l'élément HTML qui doit faire apparaître la popover.
La directive va devoir :
  1. Ajouter les événements souris via parent.on(...) pour faire apparaître/disparaître la popover,
  2. Avoir de faire apparaître la popover, il va falloir la positionner correctement ! C'est à dire centrer sur l'élément parent (on aurait pu centrer sur la souris),
  3. Mémoriser la popover affichée. En effet, on souhaite qu'il n'y ait qu'une popover d'affichée à la fois.

Pour ce qui est de calculer la position de la popover, on a recours au service $mdUtil.
En effet, Angular JS utilise par défaut JQLite qui ne permet pas de récupérer comme JQuery les informations dont nous avons besoin.

Pour pouvoir utiliser du code HTML, nous utilisons l'option translude.
On choisit aussi de remplacer à la compilation la balise mdPopover par le code HTML du template via l'option replace.

Le code de la directive :
/**
 * Popover button.
 *
 * Partial code from Material Design mdTooltip.
 */
hesperidesModule.directive('mdPopover', function ($mdUtil, $mdPopoverService) {
    return {
        restrict: 'E',
        scope: true,
        transclude: true,
        replace: true,
        template: '
', link: function (scope, element) { var parent = element.parent(); var popover = element; // Display popup parent.on('mouseenter', function() { var tooltipParent = angular.element(document.body); var tipRect = $mdUtil.offsetRect(popover, tooltipParent); var parentRect = $mdUtil.offsetRect(parent, tooltipParent); var newPosition = { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2, top: parentRect.top - tipRect.height }; popover.css({ left: newPosition.left + 'px', top: newPosition.top + 'px' }); if ($propertyToolButtonService.currentPopup) { $propertyToolButtonService.currentPopup.removeClass('popover-hover'); } popover.addClass('popover-hover'); $propertyToolButtonService.currentPopup = popover; }); // Hide popup popover.on('mouseleave', function() { popover.removeClass('popover-hover'); $propertyToolButtonService.currentPopup = null; }); } }; });

Le code du service :
/**
 * Popover button.
 *
 * Service to remember current popover display.
 * Service is singleton.
 */
hesperidesModule.factory('$mdPopoverService', [function(){
    return { currentPopup: null };
}]);