salix/front/core/components/tooltip/tooltip.js

256 lines
7.0 KiB
JavaScript

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 = `<vn-tooltip class="text"><span translate>{{::text}}</span></vn-tooltip>`;
$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);