UNPKG

@spalger/kibana

Version:

Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic

147 lines (120 loc) 3.89 kB
var _ = require('lodash'); var $ = require('jquery'); var OFFSET = 10; var $clone; // translate css properties into their basic direction var propDirs = { top: 'north', left: 'west' }; function positionTooltip(opts, html) { if (!opts) return; var $chart = $(opts.$chart); var $el = $(opts.$el); var $window = $(opts.$window || window); var $sizer = $(opts.$sizer); var prev = $chart.data('previousPlacement') || {}; var event = opts.event; if (!$chart.size() || !$el.size()) return; var size = getTtSize(html || $el.html(), $sizer); var pos = getBasePosition(size, event); var overflow = getOverflow(size, pos, [$chart, $window]); var placement = placeToAvoidOverflow(pos, prev, overflow); $chart.data('previousPlacement', placement); return placement; } function getTtSize(ttHtml, $sizer) { if ($sizer.html() !== ttHtml) { $sizer.html(ttHtml); } var size = { width: $sizer.outerWidth(), height: $sizer.outerHeight() }; return size; } function getBasePosition(size, event) { return { east: event.clientX + OFFSET, west: event.clientX - size.width - OFFSET, south: event.clientY + OFFSET, north: event.clientY - size.height - OFFSET }; } function getBounds($el) { // in testing, $window is not actually a window, so we need to add // the offsets to make it work right. var bounds = $el.offset() || { top: 0, left: 0 }; bounds.top += $el.scrollTop(); bounds.left += $el.scrollLeft(); bounds.bottom = bounds.top + $el.outerHeight(); bounds.right = bounds.left + $el.outerWidth(); return bounds; } function getOverflow(size, pos, containers) { var overflow = {}; containers.map(getBounds).forEach(function (bounds) { // number of pixels that the toolip would overflow it's far // side, if we placed it that way. (negative === no overflow) mergeOverflows(overflow, { north: bounds.top - pos.north, east: (pos.east + size.width) - bounds.right, south: (pos.south + size.height) - bounds.bottom, west: bounds.left - pos.west }); }); (window.overflows || (window.overflows = [])).push(overflow); return overflow; } function mergeOverflows(dest, src) { return _.merge(dest, src, function (a, b) { if (a == null || b == null) return a || b; if (a < 0 && b < 0) return Math.min(a, b); return Math.max(a, b); }); } function pickPlacement(prop, pos, overflow, prev, pref, fallback, placement) { var stash = '_' + prop; // list of directions in order of preference var dirs = _.unique([prev[stash], pref, fallback].filter(Boolean)); var dir; var value; // find the first direction that doesn't overflow for (var i = 0; i < dirs.length; i++) { dir = dirs[i]; if (overflow[dir] > 0) continue; value = pos[dir]; break; } // if we don't find one that doesn't overflow, use // the first choice and offset based on overflo if (value == null) { dir = dirs[0]; var offset = overflow[dir]; if (propDirs[prop] === dir) { // when the property represents the same direction // as dir, we flip the overflow offset = offset * -1; } value = pos[dir] - offset; } placement[prop] = value; placement[stash] = dir; } function placeToAvoidOverflow(pos, prev, overflow) { var placement = {}; pickPlacement('top', pos, overflow, prev, 'south', 'north', placement); pickPlacement('left', pos, overflow, prev, 'east', 'west', placement); return placement; } // expose units/helpers for testing positionTooltip.getTtSize = getTtSize; positionTooltip.getBasePosition = getBasePosition; positionTooltip.getOverflow = getOverflow; positionTooltip.getBounds = getBounds; positionTooltip.placeToAvoidOverflow = placeToAvoidOverflow; positionTooltip.removeClone = function () { $clone && $clone.remove(); $clone = null; }; module.exports = positionTooltip;