3
\$\begingroup\$

The purpose is to attach a tooltip-element to arbitrary HTML-elements.

The tooltip appears when the user hovers an element. Follows the mouse-pointer while moving over the element.

/**
 * Attaches a tooltip to every element in the result-set.
 
 * @param { string } selector - An CSS-selector.
 * @param { number } horizontalOffset - Horizontal
 *   distance to the mouse pointer.
 * @param { number } VerticalOffset - Vertical distance
 *   to the mouse pointer.
 *
 * All parameter are optional. But providing a valid
 *   selector is indispensable for the function to work.
 * 
 * Usage example: 
 *   ... })('.tooltip-item', 10, 5);
 */ 
(function(selector, horizontalOffset, verticalOffset) {
  var items;
  
  selector = selector || '.tooltip';
  horizontalOffset = horizontalOffset || 5;
  verticalOffset = verticalOffset || 5;
  
  items = document.querySelectorAll(selector);
  items = Array.prototype.slice.call(items);
  
  items.forEach(function(item) {
    // Every time the pointer moves over the element the 
    //  CSS-rule in overwritten with new values for 
    //  top and left.
    item.addEventListener('mousemove', function(e) {
      let countCssRules = document.styleSheets[0].cssRules.length;
      let newRule = selector +
        ':hover:after { display: block; ' + 
                       'left: ' + (e.offsetX + horizontalOffset) + 'px; ' +
                       'top: ' +  (e.offsetY + verticalOffset) + 'px; }';
      
      document.styleSheets[0].insertRule(newRule, countCssRules);
    });
  });
})('.tooltip-item', 10, 5);
.wrap {
  width: 1000px;
  margin: 50px auto;
  font-family: georgia, serif;
  background-color: cornsilk;
  padding: 20px 30px;
}

div.tooltip-item {
  background-color: orange;
  width: 450px;
  height: 100px;
  border: 1px solid black;
  border-radius: 8px;
  margin: 60px 10px;
}

.tooltip-item {
  position: relative;
  cursor: pointer;
}

.tooltip-item:after {
  content: attr(data-message);
  position: absolute;
  left: 105%;
  white-space: nowrap;
  display: none;
  font-size: 1.2rem;
  background-color: rgba(230, 230, 230, 0.6);
  padding: 10px 20px;
  border-radius: 16px;
  font-family: Tahoma, Verdana, Segoe, sans-serif;
  font-weight: normal;
}

div.tooltip-item:after {
  text-transform: uppercase;
}

h1.tooltip-item:after {
  background-color: cyan;
  color: #ff14ec;
  font-weight: 800;
  border-radius: 20px;
  box-shadow: 6px 6px 9px #969696;
}

p.tooltip-item:after {
  background-color: #232323;
  color: #efefef;
  font-weight: 800;
  border-radius: 6px;
  box-shadow: 0 0 0 grey;
}
<div class="wrap">
  <h1 class="tooltip-item" data-message="You are hovering the h1-element.">Demo Page - Tooltip following the Mouse-Pointer</h1> 
  <div class="tooltip-item" data-message="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."></div>
  <p class="tooltip-item" data-message="Paragraph-element with a tooltip attached to it.">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
</div>

The uncompiled Sass-code:

$fontStack: Tahoma, Verdana, Segoe, sans-serif;
$primaryColor: cornsilk;

@function getGreyValue($greyByte: 240, $opacity: 1.0) {
  @return rgba($greyByte, $greyByte, $greyByte, $opacity);
}

@mixin setTooltipStyles( $element,
                         $backgroundColor: #232323,
                         $color: #efefef,
                         $fontWeight: 600,
                         $borderRadius: 6px,
                         $shadowX: 0,
                         $shadowY: 0,
                         $shadowBlur: 0,
                         $shadowColor: grey ) {
  #{ $element }.tooltip-item:after {
    background-color: $backgroundColor;
    color: $color;
    font-weight: 800;
    border-radius: $borderRadius;
    box-shadow: $shadowX $shadowY $shadowBlur $shadowColor;
  }
}

.wrap {
  width: 1000px;
  margin: 50px auto;
  font-family: georgia, serif;
  background-color: $primaryColor;
  padding: 20px 30px;
}

div.tooltip-item {
  background-color: orange;
  width: 450px;
  height: 100px;
  border: 1px solid black;
  border-radius: 8px;
  margin: 60px 10px;
}

.tooltip-item {
  position: relative; // Essential for tooltip to work!
  cursor: pointer;
}

.tooltip-item:after {
  content: attr(data-message); // Essential for tooltip to work!
  position: absolute; // Essential for tooltip to work!
  left: 105%;
  white-space: nowrap;
  display: none;
  font-size: 1.2rem;
  background-color: getGreyValue(230, 0.6);
  padding: 10px 20px;
  border-radius: 16px;
  font-family: $fontStack;
  font-weight: normal;
}
// Use higher specificity for getting different designs.
div.tooltip-item:after {
  text-transform: uppercase;
}

@include setTooltipStyles('h1', cyan, #ff14ec, 800, 20px, 6px, 6px, 9px, rgba(150, 150, 150, 1.0));


@include setTooltipStyles('p');

Currently it is necessary to write certain mandatory code in HTML and CSS. Afterwards the JavaScript function can be used.

Should I try to concentrate more on JavaScript only? I mean: Should I try to reduce the mandatory HTML-, CSS-code to a minimun?

Or is Okay the way I have done it?

The parameter of the JavaScript-function are optional.

But actually the first parameter (CSS-selector) is mandatory for the function to work. If it isn't correct to code will silently fail.

Should I change that?

Return with an exit-code which signals an error? Perhaps even throw an exception?

Looking forward to read your opions about the points I have mentioned. Other hints would interest me too.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

Should I try to concentrate more on JavaScript only? I mean: Should I try to reduce the mandatory HTML-, CSS-code to a minimun?

I think, the code i.e. HTML and CSS are minimal. Storing the tooltip on the HTML elements is correct way rather than storing it in JavaScript. So, NO. You should not only concentrate on JavaScript.

Or is Okay the way I have done it?

Yes, the logic is correct. However, the code can be shortened(see below) when written in ES6.

But actually the first parameter (CSS-selector) is mandatory for the function to work. If it isn't correct to code will silently fail.

Should I change that?

Return with an exit-code which signals an error? Perhaps even throw an exception?

Let's look at the following statement

selector = selector || '.tooltip';

This means, if selector is falsy, selector will be .tooltip. So, there will be no case when the selector will be empty and error will be thrown. That means, there is no need to handle that error.

And, I'll also suggest to use [data-message] as default parameter. This will select all the elements having data-message attribute.


Here are some suggestions:

As let is used either the environment you're using supports ES6 or the source files are compiled to ES5.

Following features of ES6 can be used

  1. Default Parameters

    This will allow to use the assigned value to the parameter as default value if no value is passed when invoking the function.

    Now, ES5's default parameter notation

    selector = selector || '.tooltip';
    

    is no longer needed.

  2. Array.from()

    Array.from() can be used to create an array from array-like structures. To iterate over the DOM elements selected by querySelectorAll(), Array.from() can be used.

    items = document.querySelectorAll(selector);
    items = Array.prototype.slice.call(items);
    
    items.forEach(function(item) {
    

    can be written as

    Array.from(document.querySelectorAll(selector))
        .forEach(function (item) {
    

Updated Code:

(function (selector = '[data-message]', horizontalOffset = 5, verticalOffset = 5) {
    'use strict';

    Array.from(document.querySelectorAll(selector)).forEach(function (item) {
        // Every time the pointer moves over the element the
        // CSS-rule in overwritten with new values for
        // top and left.
        item.addEventListener('mousemove', function (e) {
            let countCssRules = document.styleSheets[0].cssRules.length;
            let newRule = selector +
                ':hover:after { display: block; ' +
                'left: ' + (e.offsetX + horizontalOffset) + 'px; ' +
                'top: ' + (e.offsetY + verticalOffset) + 'px; }';

            document.styleSheets[0].insertRule(newRule, countCssRules);
        });
    });
}('.tooltip-item', 10));
.wrap {
  width: 1000px;
  margin: 50px auto;
  font-family: georgia, serif;
  background-color: cornsilk;
  padding: 20px 30px;
}

div.tooltip-item {
  background-color: orange;
  width: 450px;
  height: 100px;
  border: 1px solid black;
  border-radius: 8px;
  margin: 60px 10px;
}

.tooltip-item {
  position: relative;
  cursor: pointer;
}

.tooltip-item:after {
  content: attr(data-message);
  position: absolute;
  left: 105%;
  white-space: nowrap;
  display: none;
  font-size: 1.2rem;
  background-color: rgba(230, 230, 230, 0.6);
  padding: 10px 20px;
  border-radius: 16px;
  font-family: Tahoma, Verdana, Segoe, sans-serif;
  font-weight: normal;
}

div.tooltip-item:after {
  text-transform: uppercase;
}

h1.tooltip-item:after {
  background-color: cyan;
  color: #ff14ec;
  font-weight: 800;
  border-radius: 20px;
  box-shadow: 6px 6px 9px #969696;
}

p.tooltip-item:after {
  background-color: #232323;
  color: #efefef;
  font-weight: 800;
  border-radius: 6px;
  box-shadow: 0 0 0 grey;
}
<div class="wrap">
  <h1 class="tooltip-item" data-message="You are hovering the h1-element.">Demo Page - Tooltip following the Mouse-Pointer</h1> 
  <div class="tooltip-item" data-message="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."></div>
  <p class="tooltip-item" data-message="Paragraph-element with a tooltip attached to it.">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.</p>
</div>

\$\endgroup\$
5
\$\begingroup\$

I found this post and took your code and made some changes. I wanted to have a very small piece of javascript code that supported style-able tooltips.

First, the way you were editing the CSS was to add an additional style every time the mouse moves. This will create hundreds of style additions in just a few seconds of mouse movement, so very inefficient over time. To avoid this, I created a blank style for the hover class and preserved reference to this style definition. I then modified the style using this reference, rather than creating new styles every time the handler is called.

Second, the code doesn't account for if the value is blank. The tooltip will show a blank box, instead of not showing at all. So I added a conditional to hide the tooltip if this is case.

Third, you can select a CSS style by data attribute, so there is no need to specify a .tooltip class for the element. So I dropped that.

Forth, setting the position of the tooltip element relative to the hover element has glitches. If the hover element is inside another element, the tooltip width will be glitchy and often too narrow if the parent element is something small, like a table cell. There are workarounds, such as setting a fixed width of the tooltip element, but this isn't ideal. There is another workaround to contain the tooltip in a wrapper div, but you cannot do this because html is not allowed when using the CSS content attribute. The fix I used is to set the tooltip to "display: fixed", and set position based on screen coordinates. This allows me to use max-width while shorter tooltips are only the width of the text.

A final note: this solution is probably not ideal on a mobile device. But I am using this for a desktop environment, so it works for this class of usage.

document.addEventListener('DOMContentLoaded', function() {
  var cssSheet = document.styleSheets[0];
  var hoverIndex = cssSheet.insertRule('[data-tooltip]:hover:after {}', cssSheet.cssRules.length);
  var cssHover = cssSheet.cssRules[hoverIndex];				
  Array.from(document.querySelectorAll('[data-tooltip]')).forEach(function (item) {
    item.addEventListener('mousemove', function (e) {					
      if (this.dataset.tooltip == '') {
        cssHover.style.display = 'none';
        return;
      }
      cssHover.style.display = 'block';
      cssHover.style.left = (e.clientX + 15) + 'px';
      cssHover.style.top = (e.clientY + 15) + 'px';					
    });
  });
});	
[data-tooltip] {
  border-bottom: 1px dotted #777;
  cursor: help;
}

[data-tooltip]:after {		
  content: attr(data-tooltip);
  display: none;
  position: fixed;
  max-width: 600px;
  background: #ffd;
  border: 1px solid #999;
  padding: 12px;
  color: #000;
  border-radius: 3px;
  margin: 0;	
  font-size: 14px;

  left: 50px;
  top:25px;
  z-index: 10;

  box-shadow: 0px 0px 5px rgba(0,0,0,.2);

  line-height: 1.3em;
}		
<p><span data-tooltip='I am a tooltip!'>Hover over me!</span></p>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.