CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/components/tipsy-tooltip-view.js

Summary

Maintainability
A
2 hrs
Test Coverage
var _ = require('underscore');
var CoreView = require('backbone/core-view');
require('tipsy');

/**
 *  Tipsy tooltip view.
 *
 *  - Needs an element to work.
 *  - Inits tipsy library.
 *  - Clean bastard tipsy bindings easily.
 *
 */

module.exports = CoreView.extend({
  options: {
    gravity: 's',
    opacity: 1,
    fade: true
  },

  events: {
    'mouseenter': '_onMouseEnter',
    'mouseleave': '_onMouseLeave'
  },

  initialize: function (opts) {
    if (!opts.el) throw new Error('Element is needed to have tipsy tooltip working');

    this._mouseEnterAction = opts.mouseEnterAction;
    this._mouseLeaveAction = opts.mouseLeaveAction;
    this._tipsyOpenedManually = opts.trigger === 'manual';

    this._initTipsy();
  },

  _initTipsy: function () {
    var options = _.clone(this.options);

    if (options.gravity === 'auto') {
      options.gravity = this.getOptimalGravity(
        options.gravityConfiguration && options.gravityConfiguration.preferredGravities || ['s', 'n', 'e', 'w'],
        options.gravityConfiguration && options.gravityConfiguration.margin || 0,
        window
      );
    }

    if (!options.title) {
      options.title = this.getTitleFromDataAttribute;
    }

    this.$el.tipsy(options);
    this.tipsy = this.$el.data('tipsy');
  },

  _onMouseEnter: function () {
    this._mouseEnterAction && this._mouseEnterAction();
  },

  _onMouseLeave: function () {
    this._mouseLeaveAction && this._mouseLeaveAction();
  },

  setOffset: function (offset) {
    this.tipsy.options.offset = offset;
  },

  showTipsy: function () {
    this.$el.tipsy('show');
  },

  hideTipsy: function () {
    this.$el.tipsy('hide');
  },

  getElement: function () {
    return this.el;
  },

  getOptimalGravity: function (preferredGravities, margin, browserWindow) {
    return function (tooltipElement) {
      if (!tooltipElement) {
        return preferredGravities[0] || 'n';
      }

      var tooltipContainer = this;
      var tooltipContainerBoundingRect = tooltipContainer.getBoundingClientRect();
      var gravityOptions = { n: 'bottom', s: 'top', w: 'right', e: 'left' };

      var viewportBoundaries = {
        top: browserWindow.pageYOffset + margin,
        right: browserWindow.innerWidth + browserWindow.pageXOffset - margin,
        bottom: browserWindow.innerHeight + browserWindow.pageYOffset - margin,
        left: browserWindow.pageXOffset + margin
      };

      var tooltipBoundaries = {
        top: tooltipContainerBoundingRect.top - tooltipElement.offsetHeight,
        right: tooltipContainerBoundingRect.left + tooltipContainerBoundingRect.width + tooltipElement.offsetWidth,
        bottom: tooltipContainerBoundingRect.top + tooltipContainerBoundingRect.height + tooltipElement.offsetHeight,
        left: tooltipContainerBoundingRect.left - tooltipElement.offsetWidth
      };

      var tooltipBodyGravity = '';
      var optimalGravity = _.find(preferredGravities, function (gravity) {
        var position = gravityOptions[gravity];
        var tooltipFitsPosition = isTooltipInsideViewport(position, viewportBoundaries[position], tooltipBoundaries[position]);

        if (tooltipFitsPosition && (position === 'top' || position === 'bottom')) {
          tooltipBodyGravity = getTooltipBodyGravity(gravityOptions, viewportBoundaries, tooltipBoundaries);
        }

        return tooltipFitsPosition;
      });

      return optimalGravity ? optimalGravity + tooltipBodyGravity : preferredGravities[0];
    };
  },

  getTitleFromDataAttribute: function () {
    return this.getAttribute('data-tooltip');
  },

  destroyTipsy: function () {
    if (this.tipsy) {
      // tipsy does not return this
      this.tipsy.hide();
      this.$el.unbind('mouseleave mouseenter');
    }

    if (this._tipsyOpenedManually) {
      this.hideTipsy();
    }

    this.$el.removeData('tipsy');
    delete this.tipsy;
  },

  clean: function () {
    this.destroyTipsy();
  }
});

var isTooltipInsideViewport = function (bound, viewportBound, tooltipBound) {
  if (bound === 'top' || bound === 'left') {
    return viewportBound <= tooltipBound;
  }

  if (bound === 'right' || bound === 'bottom') {
    return viewportBound >= tooltipBound;
  }
};

var getTooltipBodyGravity = function (gravityOptions, viewportBoundaries, tooltipBoundaries) {
  var nonCollisioningEdges = _.filter(['e', 'w'], function (edge) {
    var bound = gravityOptions[edge];
    return isTooltipInsideViewport(bound, viewportBoundaries[bound], tooltipBoundaries[bound]);
  });

  if (nonCollisioningEdges.length === 1) {
    return nonCollisioningEdges[0];
  }

  return '';
};