linagora/openpaas-esn

View on GitHub
frontend/js/modules/profile-popover-card/profile-popover-card.service.js

Summary

Maintainability
C
1 day
Test Coverage
(function(angular) {
  'use strict';

  angular.module('esn.profile-popover-card')
    .factory('profilePopoverCardService', profilePopoverCardService);

  function profilePopoverCardService(
    $rootScope,
    $compile,
    $modal,
    $log,
    $timeout,
    _,
    uuid4,
    session,
    touchscreenDetectorService,
    matchmedia,
    ESN_MEDIA_QUERY_SM_XS
  ) {
    var delayedFnIds = [];
    var functions = {
      bind: bind,
      _bind: _bind,
      bindPopover: bindPopover,
      _createScope: _createScope,
      createPopover: createPopover,
      showPopover: _.debounce(_showPopover, 100, { leading: true, trailing: false }),
      bindModal: bindModal,
      createModal: createModal,
      _normalizeUser: _normalizeUser,
      _get: _get
    };

    return {
      bind: functions.bind,
      functions: functions
    };

    /**
     * Will bind a popover or a modale based on whether the device is a mobile and `showMobile` option is activated
     *
     * @param {Element|jQuery|String}element HTML element on which to append the popover card
     * @param {object} userObject The user of which to display the card.
     * @param {string=} userObject.id
     * @param {string=} userObject._id
     * @param {string=} userObject.email
     * @param {string=} userObject.preferredEmail
     * @param {string=} userObject.name
     * @param {string=} userObject.displayName
     *
     * In case your user object is asynchronously population, you may want a dynamic binding, in which case,
     * you can pass the scope property defining the user to watch and a property of the user object that
     * will be observed to indicate that the user is complete.
     *
     * For instance, profile-popover-card='{ source: "ctrl.attendee", property: "id" }' will asynchronously
     * bind the popover when scope.ctrl.attendee is a correct user by using scope.$watch('ctrl.attendee.id').
     *
     * @param {string=} userObject.source
     * @param {string=} userObject.property
     *
     * @param {object=} options
     * @param {string=} options.placement Placement of popover. Either one of 'top', 'left', 'bottom' or 'right'.
     *   Defaults to 'top'
     * @param {object=} options.parentScope Current scope. Used to hide popover on scope destroy
     * @param {string=} options.eventType Event on which to bind the popover display. Defaults to 'click' on mobile
     * @param {string|Element|jQuery=} options.hideOnElementScroll Element of which scroll event will be listened;
     *   popover will be hidden on this element's scroll screens and 'mouseenter' on desktop
     *
     * see https://ci.linagora.com/linagora/lgs/openpaas/esn/merge_requests/777
     */
    function bind(element, userObject, options) {
      if (!userObject.source && !userObject.property) {
        return functions._bind(element, functions._createScope(userObject), options);
      }

      var watchExp = userObject.source + '.' + userObject.property;

      var whenReady = function(unwatch) {
        var user = functions._get(options.parentScope, userObject.source);
        var watchedProperty = functions._get(options.parentScope, watchExp);

        if (watchedProperty !== undefined) {
          var scope = functions._createScope(user);

          functions._bind(element, scope, options);
          unwatch();

          options.parentScope.$watch(userObject.source, _.partial($timeout, function() {
            var newValue = functions._get(options.parentScope, userObject.source);

            scope.user = functions._normalizeUser(newValue);
          }));
        }
      };

      var unwatch = options.parentScope.$watch(watchExp, function() {
        whenReady(unwatch);
      });
    }

    function _bind(element, scope, options) {
      var defaultOpts = {
        eventType: touchscreenDetectorService.hasTouchscreen() ? 'click' : 'mouseover',
        placement: 'top',
        showMobile: false,
        parentScope: undefined,
        hideOnElementScroll: undefined
      };
      var opts = _.assign({}, defaultOpts, options || {});

      var popover;

      if (opts.showMobile && matchmedia.is(ESN_MEDIA_QUERY_SM_XS)) {
        popover = functions.bindModal(element, scope);
      } else if (!matchmedia.is(ESN_MEDIA_QUERY_SM_XS)) {
        popover = functions.bindPopover(element, scope, opts);
      }

      if (popover) $rootScope.$on('$stateChangeStart', popover.hide);

      return popover;
    }

    function bindPopover(element, scope, options) {
      var popover = functions.createPopover(element, scope, options.placement);

      if (options.parentScope) options.parentScope.$on('$destroy', popover.hide);
      if (options.hideOnElementScroll) $(options.hideOnElementScroll).scroll(popover.hide);
      $(element).attr('title', undefined);
      $(element).css({ cursor: 'pointer' });

      element.on(options.eventType, function(evt) {
        evt.preventDefault();
        evt.stopPropagation();

        popover.show();
      });

      return popover;
    }

    function createPopover(element, scope, placement) {
      var uuid = uuid4.generate();

      var popoverTemplate = [
        '<div class="profile-card-container profile-popover-card popover" data-profile-popover-card="' + uuid + '" role="tooltip">',
        '  <div class="arrow"></div>',
        '  <div class="popover-content"></div>',
        '</div>'
      ].join('');

      scope.hideComponent = hide;

      var template = $compile('<profile-popover-content user="user" is-current-user="isCurrentUser" object-type="objectType" hide-component="hideComponent()" />')(scope);

      var $popoverOrigin = $(element).popover({
        content: template,
        placement: placement,
        container: 'body',
        trigger: 'manual',
        html: true,
        template: popoverTemplate
      });

      var $popover = function() {
        return $('.profile-popover-card[data-profile-popover-card="' + uuid + '"]');
      };

      var timeoutedHide = _.debounce(_timeoutedHide, 100);

      if (touchscreenDetectorService.hasTouchscreen()) {
        $('body').on('click', function(evt) {
          var $evt = $(evt.target);

          if (!$evt.hasClass('.profile-popover-card') && $evt.parents('.profile-popover-card').length === 0) {
            evt.preventDefault();
            evt.stopPropagation();
            hide();
          }
        });
      }

      function hide() {
        $('.profile-popover-card[data-profile-popover-card="' + uuid + '"]').popover('hide');
      }

      function _timeoutedHide() {
        if (!$('.profile-popover-card:hover').length && !$popoverOrigin.is(':hover')) {
          hide();
          $('body').off('mousemove', timeoutedHide);
        }
      }

      function show() {
        functions.showPopover($popover, $popoverOrigin, timeoutedHide);
      }

      return {
        show: show,
        hide: hide
      };
    }

    function _showPopover($popover, $popoverOrigin, timeoutedHide) {
      // Verifies that the popover the user is trying to open is not the same one as already opened
      if ($popover().is(':visible')) return;

      var promise = $timeout(function() {
        var promises = angular.copy(delayedFnIds);

        delayedFnIds.length = 0;
        promises.forEach(function(promise) {
          $timeout.cancel(promise);
        });
        $('.profile-popover-card').popover('hide');
        $popoverOrigin.popover('show');
        $('body').on('mousemove', timeoutedHide);
      }, 300);

      delayedFnIds.push(promise);
    }

    /**
     * Same as bindPopover but displays a modal
     */
    function bindModal(element, scope) {

      var modal = functions.createModal(scope);
      var eventType = touchscreenDetectorService.hasTouchscreen() ? 'click' : 'mouseover';

      element.on(eventType, function(evt) {
        evt.preventDefault();
        evt.stopPropagation();

        modal.show();
      });

      return modal;
    }

    function createModal(scope) {
      var modal = $modal({
        templateUrl: '/views/modules/profile-popover-card/profile-popover-content/profile-popover-modal.html',
        scope: scope,
        show: false
      });

      return {
        show: modal.show,
        hide: modal.hide
      };
    }

    function _normalizeUser(userObject) {
      var user = _.assign({}, userObject);

      // Normalises between people and user objects
      if (user.id) user._id = user.id;
      if (user.email) user.preferredEmail = user.email;
      if (!user.objectType) user.objectType = 'email';

      user.displayName = user.displayName || user.name || user.preferredEmail;
      user.name = user.name || user.displayName;

      return user;
    }

    /**
     * >>> _get({prop1: {prop2: {prop3: {prop4: 'val'}}}}, 'prop1.prop2.prop3.prop4')
     *     'val'
     * >>> _get({prop1: 'val'}, 'prop1.prop2.prop3.prop4')
     *     undefined
     */
    function _get(object, properties) {
      if (properties === undefined) return properties;

      var propertyList = properties;

      if (!_.isArray(propertyList)) propertyList = properties.toString().split('.');

      if (propertyList.length === 0) {
        return object;
      }

      if (object && object[propertyList[0]] !== undefined) {
        return functions._get(object[propertyList[0]], propertyList.slice(1));
      }

      return undefined;
    }

    function _createScope(user) {
      var normalizedUser = functions._normalizeUser(user);

      return angular.extend($rootScope.$new(true), {
        user: normalizedUser,
        objectType: normalizedUser.objectType,
        isCurrentUser: normalizedUser._id === session.user._id
      });
    }
  }
})(angular);