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.



Donnerstag, 25. Dezember 2014

Preserve Inputs across Page Reload via JS

I needed to reload an HTML page, internally from JavaScript, but didn't want to lose the inputs the user might have made. A browser's page reload will remove any JS code and context completely. There is no global variable or hidden element that survives a page reload where I could store my settings into.

URL parameters seem to be the only way to overcome such a situation. The following shows some JS code that provides saving and restoring of input fields using URL parameters.

The Problem

The HTML code below exposes a "Layout" button that executes the JS function pageScript.toggleLayout() when the user clicks it. The two checkboxes with id="calculateMaxiumum" and id="logging" are to customize the behavior of the layout() function, and should be preserved across the page reload. (For the real-world example look at my demo page.)

    <div>
        <input type="button" value="Layout" onclick="pageScript.toggleLayout(this);"/>
        <br/>
        <input type="checkbox" id="calculateMaximum" checked="bydefault">Use maximum width</input>
        <br/>
        <input type="checkbox" id="logging">Log to console</input>
    </div>

And below is the code of the pageScript.toggleLayout() function. When first called, it changes the layout by calling the internal layout() function. It then changes to a "layouted" state, and next time it is clicked it reloads the page to restore the former layout. In this case it tries to keep the values of the checkboxes.

 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
31
32
<script type="text/javascript">
   var pageScript = function() {
     var layouted = false;
     
     var toggleLayout = function(button) {
       if (layouted) {
         var calculateMaximum = $("#calculateMaximum").prop("checked"); // save checkbox values
         var logging = $("#logging").prop("checked");

         location.reload(); // reload page

         // following won't work to restore saved values!
         $("#calculateMaximum").prop("checked", calculateMaximum);
         $("#logging").prop("checked", logging);
       }
       else {
         layouted = true;
         layout();
         button.setAttribute('value', 'Reset Layout'); // set new label on button
       }
     };

     var layout = function() {
       ....
     };

     return {
       toggleLayout: toggleLayout
     };
   }();

</script>

Mind that I use jQuery in this code, all $(...) expressions are DOM queries via CSS expressions.
Mind further that this applies the "Revealing Module Pattern" to hide any implementation and expose just what is needed. The pageScript variable is the result of an anonymous self-executing function returning an object that exposes just toggleLayout(), all other variables and functions are hidden within the self-executing function.

This JS code calls the layout() function when not yet done, and reloads the page (to restore the previous layout) when already done.

It shows my first naive attempt to save the checkbox values. But this does not work, because after location.reload() the running JavaScript and its execution context (the window object) have been removed from the browser's memory!

So I needed some other approach to save and restore my checkboxes. The only way to save and restore page settings across a page reload are URL parameters. Because my page is a simple one that is not part of some web application (which already defines URL parameters) this is easy. I have control over the URL to be loaded, and can edit it in any way.

The URL of the currently loaded browser page is in JS variable location.href, which is a read/write variable, meaning that an assignment to location.href will load the assigned URL.

URL Parsing

This is needed when saving and restoring values via URL parameters. URL (Unified Resource Locator) and URI (Unified Resource Identifier) have been specified in so-called RFC ("Request For Comment"), see various sources on the internet for that. URL parsing is not "trivial".

Fortunately any web browser provides URL parsing in "a" elements (hyperlinks).

    var parseUrl = function(url) {
      if ( ! url  )
        return location;
        
      var hyperLink = document.createElement("a");
      hyperLink.href = url; // triggers URL parsing
      return hyperLink;
    };

This gives me access to the rough parts of an URL, which are the following for an example like

parseUrl("http://host:8080/path1/path2/servlet?one=1&two=2#somehash")
  • hyperLink.hash = #somehash
  • hyperLink.host = host:8080
  • hyperLink.hostname = host
  • hyperLink.href = http://host:8080/path1/path2/servlet?one=1&two=2#somehash
  • hyperLink.pathname = /path1/path2/servlet
  • hyperLink.port = 8080
  • hyperLink.protocol = http:
  • hyperLink.search = ?one=1&two=2
You can play with URL parsing on my demo page.

But as URL parameters are bundled with a starting "?" and "&" separators, like in ?one=1&two=2, I needed more functionality to access the values of such parameters.

Furthermore care has to be taken to encode and decode their values, because not any character is allowed in an URL (see RFC specification).

URL Parameter Access

The URL parameters are packed in the hyperLink.search variable. Here are some JS functions to manage URL parameters in relation with the parseUrl() function above.
They do not depend on jQuery (nice to copy & paste :-).

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     * Reads the value for given parameter name from given params string, containing all parameters.
     * @param name the name of the parameter.
     * @param params the string containing all URL parameters, composed like "location.search".
     * @return the value of given parameter.
     */
    var getUrlParameter = function(name, params) {
      params = params || location.search; // default to current URL parameters
      
      var match = new RegExp("[?&]"+name+"=([^&]*)").exec(params);
      if ( ! match )
        return undefined;
        
      var value = match[1].replace(/\+/g, " "); // replace every '+' by a space
      return decodeURIComponent(value);
    };
    
    /**
     * Sets the given parameter name and value to given URL string,
     * overwriting any existing value. Removes the parameter when value is empty.
     * @param name the name of the parameter to set.
     * @param value the value of the parameter to set.
     * @param url the URL, as text, where to set the parameter into.
     * @return a new URL containg the given parameter.
     */
    var setUrlParameter = function(name, value, url) {
      var parsedUrl = parseUrl(url);
      var params = parsedUrl.search;
      if (params) {
        var match = new RegExp("[?&]"+name+"=([^&]*)").exec(params);
        if (match) { // remove old name=value
          var start = params.indexOf(match[0]);
          var end = start + match[0].length;
          params = params.slice(0, start) + params.slice(end);
        }
        if (value)
          params = params+"&"+name+"="+encodeURIComponent(value);
      }
      else if (value) {
        params = "?"+name+"="+encodeURIComponent(value);
      }
      var href = bareUrl(parsedUrl);
      return href + params + (parsedUrl.hash ? parsedUrl.hash : "");
    };
    
    /**
     * Removes all URL parameters from given URL.
     * @param url the URL, as text, where to remove all parameters from.
     * @return a new URL containg no parameter.
     */
    var removeAllUrlParameters = function(url) {
      var parsedUrl = parseUrl(url);
      var bare = bareUrl(parsedUrl);
      return bare+(parsedUrl.hash ? parsedUrl.hash : "");
    };
    
    /** @return given parsed URL object as text without parameters and hash. */
    var bareUrl = function(parsedUrl) {
      var href = parsedUrl.href; // contains both params and hash
      href = parsedUrl.search ? href.slice(0, href.length - parsedUrl.search.length) : href;
      href = parsedUrl.hash ? href.slice(0, href.length - parsedUrl.hash.length) : href;
      return href;
    };

This should be everything needed to reload a page and preserve input values. The functions, their parameters and return values are decribed in heading comments.

Notes:

The replacing of every '+' by a space is to overcome some older browser's URL space treatment.

You find documentation about regular expressions on the internet.
In short, the RegExp("[?&]"+name+"=([^&]*)") reads

Starting with '?' or '&', then the parameter name, then '=', then zero-to-n characters that must not be '&'.
The parentheses around ([^&]*) are to tell the RegExp that the value of the parameter behind the '=' should be returned in the result array.

The built-in JS functions encodeURIComponent() and decodeURIComponent() have been used to pack and unpack any possible value that is stored into an URL parameter.

Integration

Following shows how my in-page script looked like after integrating these functions.
Here I use jQuery again to access the checkboxes to preserve.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<script type="text/javascript">
   var pageScript = function() {
     var layouted = false;
     
     var toggleLayout = function(button) {
       if (layouted) {
         reloadPage();
       }
       else {
         layouted = true;
         layout();
         button.setAttribute('value', 'Reset Layout'); // set new label on button
       }
     };

     var layout = function() {
       ....
     };
     
     // above URL managing functions go here
     // ....

     var reloadPage = function() {
       var urlText = removeAllUrlParameters();
       
       var calculateMaximum = $("#calculateMaximum").prop("checked");
       urlText = setUrlParameter("calculateMaximum", calculateMaximum ? "true" : "false", urlText);
       
       var logging = $("#logging").prop("checked");
       urlText = setUrlParameter("logging", logging ? "true" : "false", urlText);
       
       location.href = urlText; // reload page
     };
    
     $(document).ready(function() {
       if ( ! location.search )
         return; // no URL parameters given
       
       var calculateMaximum = getUrlParameter("calculateMaximum");
       if (calculateMaximum)
         $("#calculateMaximum").prop("checked", calculateMaximum === "true");
       
       var logging = getUrlParameter("logging");
       if (logging)
         $("#logging").prop("checked", logging === "true");
     });
     

     return {
       toggleLayout: toggleLayout
     };
   }();
   
</script>

You find the URL-managing functions and an example application on my demo page.

For a more demanding JS library to handle URLs and URL-parameters you might want to have a look on URI.js.




Samstag, 20. Dezember 2014

The Self-Displaying Page

Sometimes I had the desire to display source code from the HTML page I was writing directly in that page. Especially in Blog articles, where I discuss a lot of code. I mean, when I write an "inpage" JavaScript, copy that code, enrich it with syntax-highlighting and put it right into the same web page, that copy won't change when the original code changes. The ubiquitous copy & paste problem, one of the biggest drawbacks in software production.

Displaying parts of a page directly in that page is possible only with JavaScript. The code I present here is written without the use of jQuery, it is plain JavaScript. It won't work in InternetExplorer-8, which does not support textContent.

The Desire

  • Display the whole HTML document within itself as HTML text, including "inpage" JavaScript and CSS,
  • separately display each JavaScript being in that page (not the imported ones),
  • the same for each CSS declaration in that page, and
  • all code should be syntax-highlighted for a good visual experience.
For syntax-highlighting I first tried to use the API of an online highlighter, in particular hilite.me (which does a really nice job, thanks for that page!). My plan was to fetch some code out of the DOM, POST it to hilite.me, and then append the returned result as innerHTML into the page.
But I found out that such a technique represents cross-site scripting, which is a security hazard that browsers do not allow. So I referenced a JS highlighter library that works directly on the page: highlightjs.org.

Import Syntax Highlighting

In the head of this HTML document I wrote following imports of highlightjs:

  <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css">
  <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js"></script>

This is like described on the web page.

In a Page, Display Source Code from that Page

Now here is an example of what I am talking about. I hope it works in any browser.

Following script does not exist as HTML in this page, it is in a script tag (where it belongs to).
The HTML you see is created on the fly when you navigate to this page, done by just that JavaScript you see here:

What is happening here?

The function displayElement() creates a new pre element. A pre element preserves newlines. The function then puts the text of the given parameter element (f.i. JS text from a script tag) into that pre element, and adds it to the HTML document as child of the given target element. Finally the given highlighter function gets called, with the newly created element as parameter, this adds colors.

What remains to do is finding the parameters for this function. For that purpose I marked the JavaScript with an id = "myscript", and the destination element with id = "target". The predefined JS function document.getElementById() finds these elements. The syntax-highlighter is a function that checks if highlightjs is available and calls it on the given HTML element when so. The setAttribute("class", language) is a hint for highlightjs. Finally the function displayElement() is called with these parameters.

HTML Skeleton

If you want to try this out, here is an HTM skeleton to start with. Copy & paste the above script into to the empty script tag, save it as file and call it in your web browser via

file:///home/me/self-display.html
in case the file was saved to /home/me/self-display.html.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  
  <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css">
  <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js"></script>
  
  <script type="text/javascript" >hljs.initHighlightingOnLoad();</script>
</head>

<body>

  <h1>The Self-Displaying Page</h1>

  <div id="target"></div>

  <script id="myscript" type="text/javascript">
    // JavaScript goes here
  </script>

</body>
</html>

Maybe you also want to try out how the whole HTML document looks inside itself. Then you can add this line to the JavaScript:

displayElement(document.documentElement, "html", target, highlighter);

Here is a link to my demo page. Have fun!