kibana-123
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.92 kB
JavaScript
import _ from 'lodash';
import $ from 'jquery';
const OFFSET = 10;
let $clone;
// translate css properties into their basic direction
const propDirs = {
top: 'north',
left: 'west'
};
function positionTooltip(opts, html) {
if (!opts) return;
const $chart = $(opts.$chart);
const $el = $(opts.$el);
const $window = $(opts.$window || window);
const $sizer = $(opts.$sizer);
const prev = $chart.data('previousPlacement') || {};
const event = opts.event;
if (!$chart.size() || !$el.size()) return;
const size = getTtSize(html || $el.html(), $sizer);
const pos = getBasePosition(size, event);
const overflow = getOverflow(size, pos, [$chart, $window]);
const placement = placeToAvoidOverflow(pos, prev, overflow);
$chart.data('previousPlacement', placement);
return placement;
}
function getTtSize(ttHtml, $sizer) {
if ($sizer.html() !== ttHtml) {
$sizer.html(ttHtml);
}
const 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.
const 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) {
const 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) {
const stash = '_' + prop;
// list of directions in order of preference
const dirs = _.unique([prev[stash], pref, fallback].filter(Boolean));
let dir;
let value;
// find the first direction that doesn't overflow
for (let 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];
let 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) {
const 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;