{"id":121,"date":"2005-03-09T10:42:40","date_gmt":"2005-03-09T08:42:40","guid":{"rendered":"http:\/\/blogs.bl0rg.net\/netzstaub\/?p=121"},"modified":"2005-03-09T10:42:40","modified_gmt":"2005-03-09T08:42:40","slug":"javascript-experiences-tooltips","status":"publish","type":"post","link":"https:\/\/blogs.bl0rg.net\/netzstaub\/2005\/03\/09\/javascript-experiences-tooltips\/","title":{"rendered":"Javascript experiences: tooltips"},"content":{"rendered":"<p>\nI finally wrote my first &#8220;real&#8221; javascript program this weekend. I have a lot of articles about biology. Each article can have tables, figures, and references to other articles. Furthermore, each article has a kind of small introductory text, called the &#8220;definition&#8221;. I wanted to have tooltips showing these figures, tables and definitions when dragging the mouse over a link. For example, if a paragraph features a link to another article, I wanted to have a tooltip showing the title, author and definition of the other article showing up. This would also be the first &#8220;real&#8221; program written with my &#8220;lisp to javascript&#8221; compiler.\n<\/p>\n<p><!--more--><\/p>\n<p>\n<strong>JavaScript Tooltips using CSS<\/strong>\n<\/p>\n<p>\nAfter surfing the web, searching for a nice way to do popups in javascript, I found the following solution <a href=\"http:\/\/www.devarticles.com\/c\/a\/JavaScript\/Creating-PopUp-Notes-with-CSS-and-JavaScript-Part-I\/\">Creating Pop-Up Notes with CSS and JavaScript Part I<\/a>. The code in the article uses CSS and javascript to show\/hide and position a DIV element containing the popup elements. I used the same approach to show and hide my popups. However, I customized the part where the popup DIVs are attached to the links. Also, in some cases the DIV I wanted to show as a tooltip was already present inside the HTML page. The solution here was to clone the DIV element, to modify its properties so it would be hidden, and to reattach it to the BODY element. There can be multiple links using the same DIV as a tooltip. The tooltips are referred by an ID, which is the ID of the DIV they are created from in the HTML. The tooltips are then stored in an array, and reference by their ID. The code to find a tooltip is found in the function GET-TTIP:\n<\/p>\n<pre>\n(defvar *ttips* (array))\n\n(defun get-ttip (ttip-id)\n     (whenlet (ttip (aref *ttips* ttip-id))\n       (return ttip))\n     (whenlet (tpreview (document.get-element-by-id ttip-id))\n       (let ((ttip (tpreview.clone-node t)))\n         (setf ttip.style.visibility :hidden\n           ttip.style.position :absolute\n           ttip.style.width \"40%\"\n           ttip.style.padding \"10px\"\n           ttip.style.margin \"10px\"\n           ttip.style.top 0\n           ttip.style.left 0\n           ttip.style.z-index 1\n           ttip.id undefined\n           ttip.orig-id tpreview.id)\n         (let ((body (aref (document.get-elements-by-tag-name \"body\") 0)))\n           (body.append-child ttip))\n         (return ttip))))\n<\/pre>\n<p>\nGET-TTIP gets compiled to the following JavaScript code:\n<\/p>\n<pre>var TTIPS = [ ];\n\nfunction getTtip(ttipId) {\n   var ttip = TTIPS[ttipId];\n   if (ttip) \n         return ttip;\n   var tpreview = document.getElementById(ttipId);\n   if (tpreview) {\n      var ttip = tpreview.cloneNode(true);\n      ttip.style.visibility = \"hidden\";\n      ttip.style.position = \"absolute\";\n      ttip.style.width = \"40%\";\n      ttip.style.padding = \"10px\";\n      ttip.style.margin = \"10px\";\n      ttip.style.top = 0;\n      ttip.style.left = 0;\n      ttip.style.zIndex = 1;\n      ttip.id = undefined;\n      ttip.origId = tpreview.id;\n      var body = document.getElementsByTagName(\"body\")[0];\n      body.appendChild(ttip);\n      return ttip\n   }\n}\n<\/pre>\n<p>\nAppending to the BODY tag is important, because else the positioning using CSS will give unpredictable results. I didn&#8217;t get this at first, and kept on attaching the cloned DIVs to a random element, and got some very weird results. Another very important step is to clear the ID of the cloned DIV. Having two or more DIVs with the same ID has some very strange effects in Mozilla\/Firefox.\n<\/p>\n<p>\n<strong>Adding callback handlers<\/strong>\n<\/p>\n<p>\nThe next step, after cloning the DIVs, is to add onmouseover and onmouseout handlers for the links we want to have tooltips. As a lispy person, I immediately thought of using closures. My first attempt however was doomed to fail. I created the closures, but somehow noticed that javascript hasn&#8217;t got real closures: all my handlers were referencering the same tooltip through the captured variable. However, I was pretty sure that a closure creation time, the tooltip variable was set to the correct value. How could this happen? This prompted me to read a bit more about JavaScript closures. I found a really nice article in <a href=\"http:\/\/jibbering.com\/faq\/faq_notes\/closures.html\">the comp.lang.javascript FAQ<\/a>, as well as some articles about how horrible closures can be <a href=\"http:\/\/www.quirksmode.org\/blog\/archives\/2005\/02\/javascript_memo.html\">in respect to memory leaks<\/a>.\n<\/p>\n<p>\n<strong>An excursion into JavaScript closures<\/strong>\n<\/p>\n<p>\nMy closure problem is easily explained by looking at how javascript closures work. In javascript, everything, including arrays, functions, and of course objects, are objects. And objects are nothing more than a collection of properties, identifed by a string, which store values, which can be a number, a string, or another object. So when a function is executed, an object is created. This object has properties for the arguments passed to the function, as well as properties for the local variables created in the function. When a closure is created inside this function, its scope refers to the parent function, which itself refers to the global scope. For example, when we have the following function:\n<\/p>\n<pre>(defun test-function (a b)\n   (let ((c 1))\n      (defun inner-function (z)\n         (return (+ z c)))))\n<\/pre>\n<p>\nwhich compiles to the following javascript:\n<\/p>\n<pre>function testFunction(a, b) {\n   var c = 1;\n   function innerFunction(z) {\n         return z + c\n   }\n}\n<\/pre>\n<p>\nthe memory representation of the functions looks like this. The scope of TEST-FUNCTION refers to the global scope. Furthermore, it has the properties a, b and c. C is set to 1, A and B are filled at invocation time. innerFunction points to the function INNER-FUNCTION. When INNER-FUNCTION refers to C, the scope link is followed, and the value of C in the TEST-FUNCTION is used. And that will be the last value that C got during the invocation of TEST-FUNCTION.\n<\/p>\n<p>\nThat is the trick here. The first function I wrote to setup the callback handlers iterated over all the node, and captured the DIV variable. It looked a bit like this:\n<\/p>\n<pre>\n(defun test-function (a b)\n   (let ((func-array (array)))\n      (dolist (i (array 1 2 3 4 5 6))\n        (setf (aref func-array i)\n              (lambda (x) (+ i x))))))\n<\/pre>\n<p>\nwhich compiles to (the GENSYM-ed symbols are a bit ugly, I know):\n<\/p>\n<pre>\nfunction testFunction(a, b) {\n   var funcArray = [  ];\n   var tmpList6 = [ 1, 2, 3, 4, 5, 6 ];\n   for (var tmpI5 = 0, i = tmpList6[tmpI5]; !(tmpI5 == tmpList6.length); tmpI5 = ++tmpI5, i = tmpList6[tmpI5]) {\n      funcArray[i] = function (x) { i + x };\n   }\n}\n<\/pre>\n<p>\nThe problem with this code is that all closures in funcArray refer to the same &#8220;i&#8221; property. Thus they will all return the same. One solution to this is to create the closure in its own function, thus forcing the creation of a new scope object for the closure. The same can be done by using the WITH construct, which creates an explicit scope object. The above can be written as:\n<\/p>\n<pre>\n(defun test-function (a b)\n   (let ((func-array (array)))\n      (dolist (i (array 1 2 3 4 5 6))\n        (with ((create :i i))\n          (setf (aref func-array i)\n                (lambda (x) (+ i x)))))))\n<\/pre>\n<p>\nwhich compiles to (notice the DOLIST expansion. I didn&#8217;t use the &#8220;for (.. in ..)&#8221; construct because it won&#8217;t work on strange collection items like HtmlCollection. I didn&#8217;t search really deep into this problem though, the macro works fine for me.):\n<\/p>\n<pre>\nfunction testFunction(a, b) {\n   var funcArray = [  ];\n   var tmpList14 = [ 1, 2, 3, 4, 5, 6 ];\n   for (var tmpI13 = 0, i = tmpList14[tmpI13]; \n        !(tmpI13 == tmpList14.length); \n        tmpI13 = ++tmpI13, i = tmpList14[tmpI13]) {\n      with ({ \"i\"  :  i }) {\n         funcArray[i] = function (x) { i + x };\n      }\n   }\n}\n<\/pre>\n<p>\nThis construct will have the desired effect of creating a lexical scope for &#8220;i&#8221;. This is the approach I used, too.\n<\/p>\n<p>\n<strong>Back to callback handlers<\/strong>\n<\/p>\n<p>\nThe code for setting the handlers is both contained in a new functions and uses with, because I didn&#8217;t quite get it at the time. I&#8217;ve been to lazy to change it \ud83d\ude42 Here it is:\n<\/p>\n<pre>(defun set-ttip (node ttip)\n  (with ((create :ttip ttip))\n        (setf node.onmouseover (lambda (event) (show-note ttip event))\n              node.onmouseout (lambda () (hide-note ttip)))))\n<\/pre>\n<p>\nwhich compiles to:\n<\/p>\n<pre>\nfunction setTtip(node, ttip) {\n   with ({ \"ttip\"  :  ttip }) {\n      node.onmouseover = function (event) { showNote(ttip, event) };\n      node.onmouseout = function () { hideNote(ttip) };\n   }\n}\n<\/pre>\n<pre>\n;; this should be equally fine though\n       (defun set-ttip (node ttip)\n         (setf node.onmouseover (lambda (event) (show-note ttip event))\n               node.onmouseout (lambda () (hide-note ttip))))\n<\/pre>\n<p><strong>Setting up the tooltips<\/strong><\/p>\n<p>The links having tooltips have a special class, which can be<br \/>\neither &#8220;seealsolink&#8221; (reference to another article), &#8220;authorlink&#8221;<br \/>\n(reference to an author), &#8220;tablelink&#8221; (links a table of the current<br \/>\narticle) or &#8220;figlink&#8221; (links a figure of the current article). To<br \/>\nsetup the tooltips we search for all nodes with the TAG-NAME &#8220;A&#8221;<br \/>\nand the corresponding class. Then, the ID of the referenced object<br \/>\nis extracted from the HREF property of the node, and the<br \/>\ncorresponding DIV is search using GET-TTIP. Finally, the callbacks<br \/>\nare set up. The code looks like this (including the toplevel code<br \/>\nregistering the onload callback). We have to do this after loading<br \/>\nthe document, else the DOM tree won&#8217;t be present.<\/p>\n<pre>\n    (defun get-elements-by-tag-name-and-class-name (node tag-name class-name)\n     (let ((res (array)))\n       (do-list (elem (node.get-elements-by-tag-name tag-name))\n         (when (eql elem.class-name class-name)\n           (res.push elem)))\n       (return res)))\n \n   (defun setup-tooltips (link-type)\n     (dolist (a (get-elements-by-tag-name-and-class-name\n                 document \"a\" link-type))\n       (whenlet (match (a.href.match (regex \"([a-zA-Z0-9_]+).html$\")))\n         (let ((ttip-id (aref match 1))\n           (ttip (get-ttip ttip-id)))\n           (when ttip\n         (set-ttip a ttip))))))\n<\/pre>\n<pre>\n   (defun ttip-setup ()\n     (setup-tooltips \"seealsolink\")\n     (setup-tooltips \"tablelink\")\n     (setup-tooltips \"authorlink\")\n     (setup-tooltips \"figlink\"))\n<\/pre>\n<pre>\n   (setf window.onload ttip-setup)))))\n<\/pre>\n<p>which compiles to:<\/p>\n<pre>\nfunction getElementsByTagNameAndClassName(node, tagName, className) {\n   var res = [  ];\n   var tmpList16 = node.getElementsByTagName(tagName);\n   for (var tmpI15 = 0, elem = tmpList16[tmpI15]; !(tmpI15 == tmpList16.length); tmpI15 = ++tmpI15, elem = tmpList16[tmpI15]) {\n      if (elem.className == className) \n            res.push(elem);\n   }\n   return res\n}\nfunction setupTooltips(linkType) {\n   var tmpList18 = getElementsByTagNameAndClassName(document, \"a\", linkType);\n   for (var tmpI17 = 0, a = tmpList18[tmpI17]; !(tmpI17 == tmpList18.length); tmpI17 = ++tmpI17, a = tmpList18[tmpI17]) {\n      var match = a.href.match(\/([a-zA-Z0-9_]+).html$\/);\n      if (match) {\n         var ttipId = match[1];\n         var ttip = getTtip(ttipId);\n         if (ttip) \n               setTtip(a, ttip);\n      }\n   }\n}\nfunction ttipSetup() {\n   setupTooltips(\"seealsolink\");\n   setupTooltips(\"tablelink\");\n   setupTooltips(\"authorlink\");\n   setupTooltips(\"figlink\")\n}\nwindow.onload = ttipSetup;\n<\/pre>\n<p><strong>The callbacks, and ugly compatibility<br \/>\nhacking<\/strong><\/p>\n<p>The callbacks themselves are quite simple, but setting up<br \/>\ncross-browser compatibility is a chore (and it doesn&#8217;t even work on<br \/>\nIEMac :\/). However here it is. First we need to set up the correct<br \/>\nfunction to get the mouse coordinates.<\/p>\n<pre>\n       (defvar get-coordinates)\n       \n       (defun ie-coordinates (event)\n     (return (array (+ event.client-x document.body.scroll-left)\n            (+ event.client-y document.body.scroll-top))))\n<\/pre>\n<pre>\n       (defun not-ie-coordinates (event)\n     (return (array event.page-x event.page-y)))\n<\/pre>\n<pre>\n       (defun opera-coordinates (event)\n     (if event.page-x\n         (return (array event.page-x  event.page-y))\n         (return (array event.client-x event.client-y))))\n<\/pre>\n<pre>\n       (if window.opera\n       (setf get-coordinates opera-coordinates)\n       (if (&gt; (navigator.user-agent.index-of \"MSIE\") -1)\n           (setf get-coordinates ie-coordinates)\n           (setf get-coordinates not-ie-coordinates)))\n<\/pre>\n<p>The next step is to get the correct size of the DIVs, of the<br \/>\nwindow, the scroll position, etc&#8230; to get a nice clipping around<br \/>\nthe tooltips:<\/p>\n<pre>\n       (defun get-window-size ()\n     (if (!= window.inner-height nil)\n         (return (array inner-width inner-height))\n         (with-slots (client-height client-width) document.body\n           (return (array client-width client-height)))))\n<\/pre>\n<pre>\n       (defun get-window-scroll ()\n     (if (!= window.inner-height nil)\n         (return (array page-x-offset page-y-offset))\n         (with-slots (scroll-top scroll-left) document.body\n           (return (array scroll-left scroll-top)))))\n<\/pre>\n<pre>\n       (defun get-elem-size (elem)\n     (if elem.offset-width\n         (return (array elem.offset-width elem.offset-height))\n         (return (array elem.style.pixel-width elem.style.pixel-height))))\n<\/pre>\n<p>The callbacks themselves are quite straightforward though \ud83d\ude42<\/p>\n<pre>\n       (defun show-note (ttip event)\n     (when event\n       ;; was fuer ein geiler clipping code\n       (let ((w-x (aref (get-window-scroll) 0))\n         (w-y (aref (get-window-scroll) 1))\n         (w-width (aref (get-window-size) 0))\n         (w-height (aref (get-window-size) 1))\n         (width (aref (get-elem-size ttip) 0))\n         (height (aref (get-elem-size ttip) 1))\n         (e-x (aref (get-coordinates event) 0))\n         (e-y (aref (get-coordinates event) 1))\n         (right (*math.min (- (+ w-x w-width) 10) (+ width e-x)))\n         (bottom (*math.min (- (+ w-y w-height) 20) (+ height e-y)))\n         (left (*math.max w-x (- right width)))\n         (top (*math.max w-y (- bottom height))))\n         #+nil\n         (alert (+ \"w-x \" w-x \" w-y \" w-y \" w-width \" w-width \" w-height \" w-height\n               \" width \" width \" height \" height\n               \" e-x \" e-x \" e-y \" e-y\n               \" right \" right \" bottom \" bottom \" left \" left \" top \" top))\n         (setf ttip.style.top  (+ 10 top)\n           ttip.style.left (+ 10 left)\n           ;;ttip.style.display :block\n           ttip.style.visibility :visible)\n<\/pre>\n<pre>\n         #+nil\n         (alert (+ \"top \" ttip.style.top \" left \" ttip.style.left\n               \" padding \" ttip.style.padding \" margin \" ttip.style.margin\n               \" width \" (aref (get-elem-size ttip) 0)\n               \" height \" (aref (get-elem-size ttip) 1)))\n<\/pre>\n<pre>\n         )))\n<\/pre>\n<pre>\n       (defun hide-note (ttip)\n     (setf ttip.style.visibility :hidden))\n<\/pre>\n<p>(Note that we can use the LISP reader to comment out parts of<br \/>\nour javascript code here. This is useful when debugging, for<br \/>\nexample).<\/p>\n<p><strong>Adding a bit of CSS voodoo<\/strong><\/p>\n<p>Finally, here is the CSS code that needs to be added to have<br \/>\nnice tooltips. I&#8217;m no webdesigner, and I know that what I&#8217;ve done<br \/>\nis bad and naughty, but the CSS is riddled with absolute sizes and<br \/>\nother stuff. It looks good though \ud83d\ude42<\/p>\n<pre>\n  ;; tooltips\n       (\".artpreview\"\n    :position :absolute\n       :z-index     1\n       :width       \"50%\"\n       :background  \"#F5F6F6\"\n       :padding     \"10px\"\n       :border      \"1px solid #DFE0E0\"\n       :-moz-border-radius \"8px\"\n       :visibility  :hidden)\n       (\".artpreview .blockquote\"\n       :background \"#fff\")\n<\/pre>\n<p>This generates the following CSS:<\/p>\n<pre>\n.artpreview {\n   position:absolute;\n   z-index:1;\n   width:50%;\n   background:#F5F6F6;\n   padding:10px;\n   border:1px solid #DFE0E0;\n   -moz-border-radius:8px;\n   visibility:hidden;\n}\n<\/pre>\n<pre>\n.artpreview .blockquote {\n   background:#fff;\n}\n<\/pre>\n<p>You can see an example of all this <a href=\"http:\/\/bl0rg.net\/~manuel\/js-example\/index.html\">here<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I finally wrote my first &#8220;real&#8221; javascript program this weekend. I have a lot of articles about biology. Each article can have tables, figures, and references to other articles. Furthermore, each article has a kind of small introductory text, called &hellip; <a href=\"https:\/\/blogs.bl0rg.net\/netzstaub\/2005\/03\/09\/javascript-experiences-tooltips\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,3],"tags":[],"class_list":["post-121","post","type-post","status-publish","format-standard","hentry","category-english","category-lispstaub"],"_links":{"self":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts\/121","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/comments?post=121"}],"version-history":[{"count":0,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts\/121\/revisions"}],"wp:attachment":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/media?parent=121"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/categories?post=121"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/tags?post=121"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}