Blog-Archiv

Montag, 29. Dezember 2014

A JS Framework for Rich Text Tooltips

When you look at the amount of available JavaScript tooltip libraries on the internet you would never think of implementing tooltips by yourself. A little later, when you look at the size of some of those libraries, you are beginning to doubt. And after looking at the demos and reading the introductions some questions come up:

"Wouldn't it be better to do this by myself instead of
  • buying features I never will use,
  • fooling around with irritating APIs and undocumented parameters,
  • picking up big implementations that support browsers nobody uses any more, or
  • finding out too late that the library does not support things I took for granted
?"
Sometimes the effort for finding out about a library is bigger than solving the task by yourself. A consequence of missing specification and standardization of software on the internet, and the pressure of the free market to sell its products.

But, excuse me, why would you need JavaScript tooltips? Don't you know the HTML "title" attribute? This provides browser-native tooltips!

The weaknesses of this kind of tooltip are:

  1. they are single-line
  2. they do not support rich text attributations like HTML does
  3. you can not copy text out of a tooltip
Sometimes it disappears when you start to read it :-)

Surely I do not want to add another tooltip library to that internet abundance. But I want to know how much effort it is to write such from scratch. And I want to write a framework. In JavaScript. In passed Blogs I tried to show up the weaknesses of that language, now lets look if I can overcome them (missing encapsulation, poor inheritance, no function overloading, missing types, ....).

Framework

To exactly specify what I am talking about, what is a framework?

A basic structure underlying a system, concept, or text.
I like this definition. Frankenstein was the result of framework development, just because frightening people always was a quite successful literary concept !-)

A framework gives us the frame for what we want to do. It will provide standard solutions and avoid beginner's mistakes. For example, with a good builder framework you could build both your house and the software you have to write to afford it, and both will be perfect :-)

The most primitive form of a framework is a super-class. You can extend that super-class and overwrite some factories and methods to adapt it to a new use-case. OO languages were created to facilitate frameworks. A framework is just the plan of something, nothing, or few, concrete.

Frameworks are quite near to configuration. Configuration is done after deployment (installation), or at run-time. Framework extensions have to be ready at compile-time. But the difference gets diffuse here as there is no compile-time for JS.

A JS Tooltip Framework?

Why would I need a framework to implement JS tooltips?
Because tooltips share logic with other components like dialogs:

  • opening a window at a well visible but not obscuring location
  • preventing other windows to interfere while the one is open
  • making HTML page contents visible that were not visible before
Look at jBox. It provides tooltips that can also be used as dialogs. Both were built on a framework. Although I do not want to implement dialogs now, I want to keep my code open to such extensions.

Specification

Basically the tooltips should provide the following (hover item 1 and 2 to see examples):

  1. I want to write a "rich" tip text as HTML, in the same document where its id will be referenced by the element the tip is to be shown over
  2. or as attribute content in the element the tip is to be shown over
  3. I want to copy a tip text using mouse and keyboard (selecting text with Ctl-C)
  4. the tip should stay as long as another element is hovered
  5. when I press ESCAPE or click the mouse elsewhere, the tip should disappear
  6. the tip should appear not immediately when the mouse is over a tipped element, but after a configurable delay time
  7. no need to support browsers that do not conform to standards, so no jQuery will be involved
And I want to achieve different kinds of tooltips, either by configuration or by frameworking (hover item 1 and 2 to see examples):
  1. fixed size tips, not being sized by the browser
  2. custom styled tips, e.g. another background color, or rounded corners
  3. tips with programmatically determined text content, showing e.g. the HTML tag-name of the element

With these user stories in mind I began to implement what you might already have seen now when you hovered the list items above. As this Blog does not allow me to import the script source from another server, I've pasted it completely here (~ 400 lines of code). You can view it by browser menu item "Page Source", or try pressing Ctl-U. You find a documented and current version of that source on my demo page.

Implementation

The idea of an HTML tooltip is an initially hidden (CSS "display: none;") element that is made visible by listening to mouse-move and mouse-enter/leave events. A recursive mouse listener installation on all elements having tooltips should provide the event. That event, together with the location of the element receiving it, determines where the tip will be shown (CSS "position: absolute; left: ...px; top: ...px;").

There are different types of mouse events one can receive through an element.addEventListener() installation, and which of them and when they arrive is a little browser-specific. On the jQuery "mouseenter" page you can try that out. Thus an important capacity of the implementation must be to keep the installation of the mouse listeners overridable.

Here is a conceptual outline of my JS code. Hover it to read explanations. It is built on the idea of functional inheritance.

var tooltipManager = function()
{
  // public overridable functions and fields

  var that = {}; // return of this function

  that.delay = 1000;

  that.install = function(root, tooltip) {
    ....
  }

  .... // other publics

  // private functions and fields

  var timer = undefined;

  var clearTimer = function() {
    ....
  }

  .... // other privates
  
  return that;
};

Next is the complete outline of functions and fields I needed to implement the rich text tooltips you see on this page. I've left out only the logger variable and log() function.

Hover the items for viewing their implementations - that way you can experience the tooltip feeling :-) Maybe the days of tooltips are numbered, because they do not exist on mobile devices ...

var tooltipManager = function()

  • that.delay;

  • that.install = function(root, tooltip)
  • that.installMouseListeners = function(element, tooltip)
  • that.newTooltip = function()
  • that.buildTooltip = function()
  • that.browserClientArea = function()
  • that.location = function(tooltip, boundingClientRect, x, y)
  • that.showDelayed = function(tooltip, element, x, y)
  • that.show = function(tooltip, element, x, y)
  • that.hide = function(tooltip)
  • that.isShowing = function(tooltip)
  • that.getTooltipContent = function(element)
  • that.hasTooltip = function(element)
  • that.getTooltipIdRef = function(element)
  • that.getTooltipAttribute = function(element)

  • var timer;
  • var currentElement;

  • var clearTimer = function()
  • var setDisplaying = function(tooltip)
  • var smartCoordinate = function(coordinate, isX, boundingClientRect, tooltipGoesTopOrLeft, scrollOffset)
  • var installMouseListeners = function(element, tooltip)
    • var mouseIn = function(event)
    • var mouseOut = function(event)
  • var installTooltipListeners = function(tooltip)
  • var installElementListenersRecursive = function(element, tooltip)

Mind that private functions are not commented, but publics are documented extensively. This is because you can call them from outside, and you can override them, and doing so you must know what is expecting you. Privates on the other hand should be self-documenting in their context. I hope I've found good identifiers and names to achieve that.

Some functions are not public to be called from outside but to be overridable. There is no way to express such in JS. In Java they would be protected non-final.

Application

As you've seen, I have installed different kinds of tooltips to this Blog page (different colors, fixed size). How this is done you can see in following code sample. Hover it to read its explanation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script type="text/javascript">
  window.addEventListener("load", function() {
    var mgr = tooltipManager();

    var tooltip12 = mgr.buildTooltip();
    tooltip12.style["background"] = "#FFFF66";
    var elem1 = document.getElementById("elem-1");
    mgr.installMouseListeners(elem1, tooltip12);
    var elem2 = document.getElementById("elem-2");
    mgr.installMouseListeners(elem2, tooltip12);

    var tooltip3 = mgr.buildTooltip();
    tooltip3.style["background"] = "#FFFF66";
    tooltip3.style["width"] = "30em";
    tooltip3.style["height"] = "20em";
    var elem3 = document.getElementById("elem-3");
    mgr.installMouseListeners(elem3, tooltip3);

    var tooltip4 = mgr.buildTooltip();
    tooltip4.style["background"] = "#ADFF85";
    tooltip4.style.cssText += "border-radius: 10px;";
    var elem4 = document.getElementById("elem-4");
    mgr.installMouseListeners(elem4, tooltip4);

    var tooltipDefault = mgr.buildTooltip();
    tooltipDefault.style["background"] = "#FFFF80";
    var elemDefault = document.getElementById("elem-default");
    mgr.install(elemDefault, tooltipDefault);
  });
</script>

Here is another application that shows how dynamically created tooltips can be implemented. In this case they show the tagName of the element they are hovering, and its viewport-relative coordinates.

The secret is "override". That's what frameworks live off.

<script type="text/javascript">
  "use strict";
  
  var myTooltipManager = tooltipManager();
  myTooltipManager.logger = console;
  
  myTooltipManager.hasTooltip = function(element) {
    return true; // show tooltip on ANY element
  };
  
  var superGetTooltipContent = myTooltipManager.getTooltipContent;
  myTooltipManager.getTooltipContent = function(element) {
    if (myTooltipManager.getTooltipAttribute(element))
      return superGetTooltipContent(element); // keep values of tooltip-attribute
    
    // but override idrefs
    var rect = element.getBoundingClientRect();
    var x = Math.round(rect.left), y = Math.round(rect.top),
        w = Math.round(rect.right - rect.left), h = Math.round(rect.bottom - rect.top);
    return "&lt;"+element.tagName+"&gt; BoundingClientRect left="+x+", top="+y+", width="+w+", height="+h;
  };
  
  var tooltip = myTooltipManager.install();
  tooltip.style["background"] = "#AAFF66";
</script>

Known Bugs

If you hover the line

var installMouseListeners = function(element, tooltip)
in the source outline above, you might notice that you can not reach the tooltip to copy the code in case the tip is BELOW the element. This is because the element showing that function contains child-elements also having tooltips. Moving the mouse down towards the tooltip causes the then hovered child element to pop up its tooltip.

Workaround: make the tooltip appear ABOVE the element. There won't be child elements. You can achieve this by scrolling the page down until the element is below the middle of the viewport. (Tooltips always try to show on that side of the element or mouse point where the most space is.)

Summary

I've written, including documentation, approximately 400 lines of JS code. (Much more I've written to test it, and to document it on this Blog.) The tips behave quite nice. Colors, borders, shapes are a question of taste and will be done per-page, one can apply any style to the tooltip without changing the JS code.

Make this all-browser-compatible? I've tested it with Firefox, Chrome, and Opera. Future will show whether addEventListener() and all the other standardization proposals will prevail (I believe they will).

An interesting question would be if I can add an optional close-timer by extending the existing implementation without changing it. Maybe this will become a follower Blog :-)


ɔ⃝  This code is free of any legal regulations.



Keine Kommentare: