import ngModule from '../../module'; import Component from '../../lib/component'; import {kebabToCamel} from '../../lib/string'; import './style.scss'; let positions = ['left', 'right', 'up', 'down']; /** * A simple tooltip. * * @property {String} position The relative position to the parent */ export default class Tooltip extends Component { constructor($element, $scope, $timeout) { super($element, $scope); this.$timeout = $timeout; $element.addClass('vn-tooltip vn-shadow'); this.position = 'down'; this.margin = 10; } /** * Shows the tooltip. * * @param {HTMLElement} parent The parent element */ show(parent) { this.parent = parent; this.$element.addClass('show'); this.relocate(); this.cancelTimeout(); this.relocateTimeout = this.$timeout(() => this.relocate(), 50); } /** * Hides the tooltip. */ hide() { this.$element.removeClass('show'); this.cancelTimeout(); } cancelTimeout() { if (this.relocateTimeout) { this.$timeout.cancel(this.relocateTimeout); this.relocateTimeout = null; } } /** * Repositions the tooltip acording to it's own size, position and parent location. */ relocate() { let axis; let position = this.position; if (positions.indexOf(position) == -1) position = 'down'; switch (position) { case 'right': case 'left': axis = 'x'; break; default: axis = 'y'; } let arrowSize = this.margin; let tipMargin = this.margin; let rect = this.parent.getBoundingClientRect(); let tipRect = this.element.getBoundingClientRect(); let tipComputed = this.window.getComputedStyle(this.element, null); let bgColor = tipComputed.getPropertyValue('background-color'); let min = tipMargin; let maxTop = this.window.innerHeight - tipRect.height - tipMargin; let maxLeft = this.window.innerWidth - tipRect.width - tipMargin; // Coordinates let top; let left; function calcCoords() { top = rect.top; left = rect.left; if (axis == 'x') top += rect.height / 2 - tipRect.height / 2; else left += rect.width / 2 - tipRect.width / 2; switch (position) { case 'right': left += arrowSize + rect.width; break; case 'left': left -= arrowSize + tipRect.width; break; case 'up': top -= arrowSize + tipRect.height; break; default: top += arrowSize + rect.height; } } calcCoords(); // Overflow let axisOverflow = axis == 'x' && (left < min || left > maxLeft) || axis == 'y' && (top < min || top > maxTop); function switchPosition(position) { switch (position) { case 'right': return 'left'; case 'left': return 'right'; case 'up': return 'down'; default: return 'up'; } } if (axisOverflow) { position = switchPosition(position); calcCoords(); } function range(coord, min, max) { return Math.min(Math.max(coord, min), max); } if (axis == 'x') top = range(top, min, maxTop); else left = range(left, min, maxLeft); let style = this.element.style; style.top = `${top}px`; style.left = `${left}px`; // Arrow position if (this.arrow) this.element.removeChild(this.arrow); let arrow = document.createElement('div'); arrow.className = 'arrow'; arrow.style.borderWidth = `${arrowSize}px`; let arrowStyle = arrow.style; if (axis == 'x') { let arrowTop = (rect.top + rect.height / 2) - top - arrowSize; arrowStyle.top = `${arrowTop}px`; } else { let arrowLeft = (rect.left + rect.width / 2) - left - arrowSize; arrowStyle.left = `${arrowLeft}px`; } let arrowCoord = `${-tipMargin}px`; switch (position) { case 'right': arrowStyle.left = arrowCoord; arrowStyle.borderRightColor = bgColor; arrowStyle.borderLeftWidth = 0; break; case 'left': arrowStyle.right = arrowCoord; arrowStyle.borderLeftColor = bgColor; arrowStyle.borderRightWidth = 0; break; case 'up': arrowStyle.bottom = arrowCoord; arrowStyle.borderTopColor = bgColor; arrowStyle.borderBottomWidth = 0; break; default: arrowStyle.top = arrowCoord; arrowStyle.borderBottomColor = bgColor; arrowStyle.borderTopWidth = 0; } this.element.appendChild(arrow); this.arrow = arrow; } $onDestroy() { this.hide(); } } Tooltip.$inject = ['$element', '$scope', '$timeout']; ngModule.vnComponent('vnTooltip', { template: require('./tooltip.html'), controller: Tooltip, transclude: true, bindings: { position: '@?' } }); directive.$inject = ['$document', '$compile']; export function directive($document, $compile) { return { restrict: 'A', link: function($scope, $element, $attrs) { let tooltip; let $tooltip; let $tooltipScope; if ($attrs.tooltipId) { let tooltipKey = kebabToCamel($attrs.tooltipId); tooltip = $scope[tooltipKey]; if (!(tooltip instanceof Tooltip)) throw new Error(`vnTooltip id should reference a tooltip instance`); } else { $tooltipScope = $scope.$new(); $tooltipScope.text = $attrs.vnTooltip; let template = `{{::text}}`; $tooltip = $compile(template)($tooltipScope); $document.find('body').append($tooltip); tooltip = $tooltip[0].$ctrl; } if ($attrs.tooltipPosition) tooltip.position = $attrs.tooltipPosition; $element[0].title = ''; $element.on('mouseover', function(event) { if (event.defaultPrevented) return; tooltip.show($element[0]); }); $element.on('mouseout', function() { tooltip.hide(); }); $element.on('$destroy', function() { tooltip.hide(); if ($tooltip) { $tooltip.remove(); $tooltipScope.$destroy(); } }); } }; } ngModule.directive('vnTooltip', directive);