UNPKG

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
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;