canoejs
Version:
A lightweight, widget-based UI framework
155 lines (136 loc) • 5.22 kB
text/typescript
import Widget from "../Widget";
import EventLinker from "../EventLinker";
import { ThemeProvider } from "../theme/ThemeProvider";
export default class Tooltip extends Widget {
classes: string[];
css: {};
callbacks: {};
text: string;
position: 'top' | 'bottom' | 'left' | 'right';
trigger: 'hover' | 'click';
show: boolean;
target: HTMLElement | null;
constructor(
opts: Partial<{
id: string,
classes: string[],
css: {},
callbacks: {},
text: string,
position: 'top' | 'bottom' | 'left' | 'right',
trigger: 'hover' | 'click',
show: boolean,
target: HTMLElement,
}> = {}
) {
super(opts);
this.classes = opts.classes ?? [];
this.css = opts.css ?? {};
this.callbacks = opts.callbacks ?? {};
this.text = opts.text ?? 'Tooltip';
this.position = opts.position ?? 'top';
this.trigger = opts.trigger ?? 'hover';
this.show = opts.show ?? false;
this.target = opts.target ?? null;
}
render(): HTMLElement {
const theme = ThemeProvider.getTheme();
const tooltip = document.createElement("div");
tooltip.id = this.id;
tooltip.className = 'tooltip';
tooltip.textContent = this.text;
// Base styles
Object.assign(tooltip.style, {
position: 'absolute',
backgroundColor: theme.colors.gray[900],
color: theme.colors.white,
padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
borderRadius: theme.borderRadius.md,
fontSize: theme.typography.fontSize.sm,
fontWeight: theme.typography.fontWeight.normal,
boxShadow: theme.shadows.md,
zIndex: '1000',
pointerEvents: 'none',
opacity: '0',
transition: 'opacity 0.2s ease-in-out',
maxWidth: '200px',
wordWrap: 'break-word',
...this.css
});
// Position styles
this.updatePosition(tooltip);
// Show/hide logic
if (this.show) {
tooltip.style.opacity = '1';
}
// Add custom callbacks
Object.keys(this.callbacks).forEach((key) => {
EventLinker.addEvent(tooltip, key, this.callbacks[key]);
});
return tooltip;
}
private updatePosition(tooltip: HTMLElement): void {
if (!this.target) return;
const targetRect = this.target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let top = 0;
let left = 0;
switch (this.position) {
case 'top':
top = targetRect.top - tooltipRect.height - 8;
left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
break;
case 'bottom':
top = targetRect.bottom + 8;
left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
break;
case 'left':
top = targetRect.top + (targetRect.height / 2) - (tooltipRect.height / 2);
left = targetRect.left - tooltipRect.width - 8;
break;
case 'right':
top = targetRect.top + (targetRect.height / 2) - (tooltipRect.height / 2);
left = targetRect.right + 8;
break;
}
// Ensure tooltip stays within viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (left < 0) left = 8;
if (left + tooltipRect.width > viewportWidth) left = viewportWidth - tooltipRect.width - 8;
if (top < 0) top = 8;
if (top + tooltipRect.height > viewportHeight) top = viewportHeight - tooltipRect.height - 8;
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
}
showTooltip(): void {
this.show = true;
const tooltipElement = document.getElementById(this.id);
if (tooltipElement) {
tooltipElement.style.opacity = '1';
this.updatePosition(tooltipElement);
}
}
hideTooltip(): void {
this.show = false;
const tooltipElement = document.getElementById(this.id);
if (tooltipElement) {
tooltipElement.style.opacity = '0';
}
}
attachTo(element: HTMLElement): void {
this.target = element;
if (this.trigger === 'hover') {
element.addEventListener('mouseenter', () => this.showTooltip());
element.addEventListener('mouseleave', () => this.hideTooltip());
} else if (this.trigger === 'click') {
element.addEventListener('click', () => {
if (this.show) {
this.hideTooltip();
} else {
this.showTooltip();
}
});
}
}
}