I'm showing a tooltip when you hover an element with the tooltip
attribute. E.g. <div class="example" tooltip="Hello World">Hover me.</div>
I also have to set the position of the tooltip based on its width, and because I use width: max-content
for the element, I have to do something like shown in the code example.
Is there any way I can make my code more efficient? I feel like I'm making it unnecessarily complicated.
(function() {
var tooltip = document.querySelectorAll("[tooltip]");
callback = (element, index, array) => {
tooltip[index].onmouseenter = () => {
let data = element.getAttribute("tooltip");
let child = document.createElement("div");
child.className = "tooltip";
child.innerHTML = data;
element.appendChild(child);
child.style.left = "50%";
child.style.marginLeft = - (child.offsetWidth / 2) + "px";
}
tooltip[index].onmouseleave = () => {
if(element.getElementsByClassName("tooltip")[0] != undefined) {
element.removeChild(element.getElementsByClassName("tooltip")[0]);
}
}
}
tooltip.forEach(callback);
})();
/* Don't review this CSS; it was added just for the demo. */
div[tooltip] { width: max-content; margin: auto; }
<div class="example" tooltip="Hello World">Hover me.</div>
2 Answers 2
For one, it's a bit wasteful to always create a new element for each tooltip; you could create one when the page loads, and just reuse that (since the user will only ever see one tooltip at the time).
The biggest pitfall, though, is that if you add new elements to the page programmatically, the event handlers won't be applied to them. E.g. if you do something like this:
let newButton = document.createElement('button');
newButton.textContent = 'click me!';
newButton.setAttribute('tooltip', 'Click here to do stuff');
newButton.onclick = () => {
alert('You did stuff');
}
document.body.appendChild(newButton);
This button, although it has a tooltip
attribute, will not show the tooltip when hovered.
You can solve this by taking advantage of event propagation: instead of setting the event handler on each individual element, you can just set one event handler on the window
object, and handle it there.
(function() {
// Create the tooltip element we will be reusing
let tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.style.left = '50%';
tooltip.style.transform = 'translateX(-50%)';
// Prepare the mouseout event handler
let mouseoutHandler = (event) => {
event.target.removeChild(tooltip);
};
// Set the mouseover event handler
window.addEventListener('mouseover', event => {
if (!event.target.dataset.tooltip) return true;
tooltip.textContent = event.target.dataset.tooltip;
event.target.appendChild(tooltip);
event.target.addEventListener(
'mouseout',
mouseoutHandler,
{ once: true }
);
});
})();
In this version, we capture the mouseenter
event on the window level, and only set the mouseleave
event handler to fire once (by passing the option { once: true }
). This also means you don't have to check if there is a tooltip inside the target element, since it is guaranteed that there will be, and the event handler won't get invoked after that until the next time we show the tooltip.
Also, instead of getAttribute()
I used the dataset
property. This also means you'll have to use <button data-tooltip="click here">
instead of <button tooltip="click here">
. It's a bit longer, but it is recommended to always use the data-
prefix for custom attributes; for one, because this way you can access the attribute directly in the dataset
property as seen above, and also to make sure your custom attribute doesn't end up colliding with any standard attribute that might be introduced later (especially for something as common as "tooltip").
I also used pure CSS to center the tooltip.
-
-
\$\begingroup\$ @Kruga Oh! Well spotted, thanks. Editing my answer now. \$\endgroup\$Máté Safranka– Máté Safranka2018年08月15日 15:24:11 +00:00Commented Aug 15, 2018 at 15:24
Don't do anything in JavaScript that can be done with just CSS! The :hover
pseudo-class is perfect for tooltips. Use ::after
to append a pseudo-element with the tooltip content, where the content is specified by the content
property.
div[tooltip]:hover::after {
content: attr(tooltip);
display: block;
position: absolute;
transform: translateX(-50%);
}
/* Don't review this CSS; it was added just for the demo. */
div[tooltip] { width: max-content; margin: auto; }
<div class="example" tooltip="Hello World">Hover me.</div>