Blog-Archiv

Freitag, 27. Oktober 2017

New HTML-5 Elements DETAILS and DIALOG

The new HTML-5 DETAILS and DIALOG elements are supported now by most web-browsers. When you don't see a triangle-right below, your current browser doesn't support it.


Click to see a simple DETAILS example.

This is hidden content ...


<details>
  <summary>Click to see a simple <code>DETAILS</code> example.</summary>
  <p>This is hidden content ...</p>
</details>

An expand-control that works without scripting and arrow-resources!



You clicked me?!

What do you want?

I told you to not disturb me!

So now get out.


<button onclick="toggleDialog('dialog');">Click to see a simple <code>DIALOG</code> example.</button>
<br>

<dialog id="dialog" onclick="toggleDialog('dialog');">
  <p>You clicked me?!</p>
  <p>What do you want?</p>
  <p>I told you to not disturb me!</p>
  <p>So now get out.</p>
</dialog>

<script>
  var toggleDialog = function(elementId) {
    var dialog = document.getElementById(elementId);
    if (dialog.getAttribute("open") !== "")
      dialog.setAttribute("open", "");
    else
      dialog.removeAttribute("open");
  };
</script>

This needs some scripting to close the dialog upon various events, in this example by clicking onto it, or clicking onto its trigger button.


How to Control Them

Both elements toggle their visibility when the attribute

open

is set into them, or removed from them. In the DIALOG example you can see (in the script code) how this is done.

If you set the open attribute in HTML code, it would be initially open. The attribute doesn't need to have a value, it just needs to be present (unlike in XML).

DETAILS

Unfortunately the DETAILS element is not animated. Like the CSS display property, it does not support the CSS transition property.

Everything else is quite straight forward. You also can build trees with this.

Animals
Mammals
Cats
Garfield
Catbert
Dogs
Lassie
Bessy
Mice
Others
Fish
Capt'n Nemo
Fisheye

 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
<style>
  details.tree {
    padding-left: 1em;
  }
  div.tree {
    padding-left: 2em;
  }
</style>

<details class="tree"><summary>Animals</summary>
  <details class="tree"><summary>Mammals</summary>
    <details class="tree"><summary>Cats</summary>
      <div class="tree">
        <div>Garfield</div>
        <div>Catbert</div>
      </div>
    </details>
    <details class="tree"><summary>Dogs</summary>
      <div class="tree">
        <div>Lassie</div>
        <div>Bessy</div>
      </div>
    </details>
    <details class="tree"><summary>Mice</summary>
    </details>
    <div class="tree">Others</div>
  </details>
  <details class="tree"><summary>Fish</summary>
    <div class="tree">
      <div>Capt'n Nemo</div>
      <div>Fisheye</div>
    </div>
  </details>
</details>

DIALOG

The DIALOG element may require a little JS programming. Primarily you need to decide which user gestures should close the ....

Possible Close Events
X

gestures
mouse
click
outside
inside
both outside or inside
onto "X" button in title bar
move
outside
keyboard
ESCAPE
ENTER
any keypress
timeout
4 seconds

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<dialog open id="dialog2" style="border: 3px solid gray; border-radius: 1em;">

  <div>
    <b>Possible Close Events</b>
    <div
        style="float: right; font-family: sans-serif; font-size: smaller; cursor: pointer;"
        onclick="toggleDialog('dialog2');"
        title="Close">
      X
    </div>
  </div>

  <hr>

  ....

</dialog>

The dialog's position is absolute, that means it doesn't take part in the layout flow. By default it will display vertically on the position where the layout contains it, and horizontally centered. Elements below the dialog will be covered by it. Therefore you always will need to program a way to close it.

By default the dialog has no title-bar, but you can style it fully, except the CSS position property. In the example shown here I added a traditional "Close" button.

Such a dialog could be used as rich-text custom tooltip, or context menu!

Summary

HTML-5 comes with a lot of new and nice features, and moves toward application programming. Web-applications don't need to import tons of JS and CSS user-interface resources any more. Nearly every desktop-application control is available also in browsers now. Unfortunately animations are still not supported out-of-the-box for open/close elements like DETAILS and DIALOG.




Sonntag, 22. Oktober 2017

Unsafe JS Namespaces

Namespaces are scopes in which names are unique. They help to organize source code into modules, packages, components, whatever. Examples for a Java namespaces are package names, like java.lang or java.util. In java.lang there can be just one class Runtime, while there could be a class of same name in another package like my.applications.namespace.

The JavaScript (JS) language does not have namespaces, thus developers handicrafted something similar. This Blog is about the JS namespace replacement, and its potential gotchas.

Nested Objects as Namespaces

Instead of namespaces, JS developers use nested objects. Everything inside a JS object is public. That means there is no encapsulation, but the possibility to have identical function- or variable-names in different contexts.

var namespace = namespace || {};
namespace.demo = namespace.demo || {};
namespace.demo.lifeModule = namespace.demo.lifeModule || {};

namespace.demo.lifeModule.love = function() {
  alert("Love is an extension of life :-)");
};

This is a typical JS namespace initialization sequence. It must be built level by level. The first line uses the var keyword, it makes sure that a variable namespace (first level) is defined in the global variable context. If it is not yet existing (going into the "||" part), a new object is created onto that name.

Then the nested object demo is set into it, representing the next namespace identifier (second level). Again a check makes sure that the space is not overwritten when already existing. The second line doesn't need the leading var keyword, because it is building into an already existing parent-object.

This code silently assumes that the variable "namespace", in case it already existed, was an object (JS functions are also objects). Should it have been an integer or a string, this instruction will fail silently, and potential following ones may display a console error.

That kind of namespace building can continue to any level. A popular concept is having

application.page.module

So above example would refer to an application named "namespace", having a page "demo", and a module "lifeModule" in that page. Inside the module is a public function called "love".

Example

Here is an example that loads two JS files, both defining the same namespace.

On the following panel you can try out what this example does.

HTML source is:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>JS Namespaces</title>
  
    <script type="text/javascript" src="namespace_life_love.js">
    </script>
  
    <script type="text/javascript" src="namespace_life.js">
    </script>
  
  </head>
       
  <body>
  
    <div style="text-align: center;">
      <button onclick="namespace.demo.lifeModule.birth();">Birth</button>
      <button onclick="namespace.demo.lifeModule.love();">Love</button>
      <button onclick="namespace.demo.lifeModule.death();">Death</button>
    </div>
    
  </body>
</html>

The button "Love" has its callback in JS file

namespace_life_love.js
var namespace = namespace || {};
namespace.demo = namespace.demo || {};
namespace.demo.lifeModule = namespace.demo.lifeModule || {};

namespace.demo.lifeModule.love = function() {
  alert("Love is an extension of life :-)");
};

The buttons "Birth" and "Death" have their callbacks in a revealing module singleton in JS file

namespace_life.js
var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = (function()
{
  var born;
  var dead;
  
  var birth = function() {
    if ( born )
      return error("Can not be born again when not yet dead!");
    
    born = true;
    dead = false;
    alert("Getting born ...");
  };
  
  var death = function() {
    if ( dead )
      return error("Can not die when already dead!");
    
    alert("Dying ...");
    dead = true;
    born = false;
  };
  
  var error = function(message) {
    alert(message);
  };
  
  
  var returnObject = namespace.demo.lifeModule || {};
  
  returnObject.birth = birth;
  returnObject.death = death;
  
  return returnObject;

})();

This is a "stateful" module, that means is maintains a state in its private variables born and dead. It exports its public functions in a return statement after having built its internal state.

Mind that again, like in the top-lines, it checks whether the namespace object already exists, and merges into the existing one when so. For that check, you have to repeat the namespace at this unusual place on bottom of the module!

Now, why are such namespaces unsafe?

The Gotcha

Normally a revealing module exports its public functions in this way:

var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = (function()
{
  ....
  
  return {
    birth: birth,
    death: death
  };

})();

This overwrites any existing namespace-object with its return. It does not merge into the existing one. Now it depends on the load-order of the script files in HTML. In the example above it would go wrong, because namespace_life.js is loaded after namespace_life_love.js!

Another hazard is this initialization variant:

var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = namespace.demo.lifeModule || (function()
{
  ....
  
  return {
    birth: birth,
    death: death
  };

})();

This tries to work around redundant initialization by testing for existence in the first line of the module definition. What happens is that the namespace_life_love.js object is already there, and the namespace_life.js functions would not be built at all!

Summary

Although a safe variant of namespace-initialization exists also for revealing modules, you are forced to repeat the namespace on bottom of the module definition. Nobody would expect it there.

Whatever you do in JS, it is unsafe. The language missed some fundamental programming language features like dependency management, typing and namespaces. In ES-6, the follow-up language of JS, namespaces and dependency management will be covered by a completely new module-concept.




Sonntag, 8. Oktober 2017

JS Dropdown Gotcha

The difference between a popup and a dropdown may be that the popup doesn't have a visible trigger, it pops up wherever the mouse was right-clicked, while a dropdown appears on left mouse click on a specific visible trigger button.
Popup also may be a synonym for a dialog window.
Popup menus are also called context menus, because they are frequently used in bulk views like lists, tables, trees and tree-tables, where the triggered menu action refers to the selected items only (→context).

This Blog is about a simple dropdown list implementation for a web page, and why it doesn't always work as expected. By the way, you can always use the browser-native dropdown element!

A Simple Dropdown

Demo

Left-click onto the buttons below to see dropdowns.

Tests:
  1. Click a "Dropdown".
    Expected: a dropdown list gets visible.

  2. Open a "Dropdown". Click anywhere but not in a "Dropdown" or a scrollbar.
    Expected: the open dropdown closes.

  3. Open a "Dropdown". Open another "Dropdown".
    Expected: first dropdown closes when second opens.

Source Code

HTML

    <div class="dropdown-button">
      Dropdown 1
      <div class="dropdown-content">
        <a href="#home">Home 1</a><br>
        <a href="#about">About 1</a><br>
        <a href="#contact">Contact 1</a>
      </div>
    </div>

The dropdown trigger is marked with CSS class dropdown-button. The dropdown-list is a nested element of the trigger, tagged by CSS class dropdown-content.

CSS

      .dropdown-content {
        display: none; /* initially invisible */
        position: absolute; /* not in layout flow */

        /* following is not important */
        border: 1px solid gray;
        background-color: white;
        padding: 0.3em;
      }
      .dropdown-button {
        /* not important */
        display: inline-block;
        border: 1px solid red;
        padding: 0.3em;
      }
      .dropdown-button::after {
        content: "\25BC"; /* the right-side arrow */
      }

The CSS is not really important here, except position: absolute, which takes the element out of the normal layout flow, and display: none, which makes it invisible and being ignored by the layout.

JavaScript

    (function() {
    
      var closeAllDropdowns = function(event, exceptElement) {
          var dropdownContents = document.getElementsByClassName("dropdown-content");
          for (var i = 0; i < dropdownContents.length; i++) {
            var dropdownContent = dropdownContents[i];
            if (exceptElement !== dropdownContent)
              dropdownContent.style.display = "none";
          }
      };
      
      var clickDropdownButton = function(event) {
          event.stopPropagation();
          
          var contentParent = (event.target || event.srcElement);
          var dropdownContent = contentParent.querySelector(".dropdown-content");
          var isHidden = (window.getComputedStyle(dropdownContent).display === "none");
          
          closeAllDropdowns(event, dropdownContent);
          dropdownContent.style.display = isHidden ? "block" : "none";
      };
        
      /* initialization: find all dropdowns on page and prepare them */
            
      var dropdownButtons = document.getElementsByClassName("dropdown-button");
      for (var i = 0; i < dropdownButtons.length; i++)
        dropdownButtons[i].addEventListener("click", clickDropdownButton); 
      
      window.addEventListener("click", closeAllDropdowns);
      
    })();

The closeAllDropdowns() function closes all dropdowns on page. It can be called with two optional parameters, the second being a dropdown that should be ignored.

The clickDropdownButton() function is called when the dropdown-button gets clicked. Because a window-listener for clicks would close all dropdowns, it stops the event bubbling. Then it finds the dropdown from the event target and displays or closes it, depending on current state, after having closed all other possibly open dropdowns.

The initialization loops all dropdown-buttons and installs click-listeners to them that toggle the dropdown-content. Another click-listener gets installed onto the window object, closing any possibly open dropdown. (Mind that other elements on the page could stop the propagation of clicks onto them, and thus prevent the dropdowns closing!)

The Gotcha

Now somebody, for some reason, changes the CSS of the dropdown-button to following:

      .dropdown-button {
        position: relative;
        overflow: hidden;

        .... /* rest unchanged */
      }

Here is the result. I made the button a little bigger to show the obscured dropdown inside. Click to see it.

What happened?

The dropdown-content was positioned absolute, so it gets positioned relatively to the nearest parent with a non-static position. For the original implementation this was the body element. Because no top and left coordinate had been put onto the dropdown-content, this worked, it kept its original coordinates and was displayed at the correct location.

But now the nearest parent with a non-static position is the dropdown-button, due to position relative. This alone does no harm, because the coordinates of the dropdown-content still are the original ones. But the the dropdown-button was also restricted to hide its overflow. No child element can go outside of such a parent's bounds.

To fix this, you must remove either position: relative or overflow: hidden. If this is not possible, rewrite the dropdown to the solution that I presented in my last Blog.

Resume

One thing always comes to my mind when I meet such problems:

  • CSS is nice for colors, borders, fonts and icons, but not for doing layout!



Samstag, 7. Oktober 2017

JS Popup Window

Popup menus, nowadays called context menus, were always some kind of challenge, on any platform. A lot of bugs occurred with them, either because the API was unclear, or because they did not work properly. I remember that the AWT PopupMenu of an early LINUX Java could freeze the whole application.

Popup menu sister is the dropdown list, a further relative is the combo box. (Also the Java/Swing JComboBox has an eventful history.) All these show a clickable window that temporarily hovers above the user interface.

Why is that window so problematic? Because it is not contained in the component-tree of the user interface, it is not in the layout, it is invisible and displays just on a certain user gesture, usually a right mouse click, or Shift-F10 key. Then it needs to gain some position where the user really can see and use it, which is more difficult than expected, especially in multi-screen environments. Finally it must be removed by events that may not have been sent to the component that contained it. (Imagine an open context menu, and then you focus another application on your desktop. Would you expect it to still be open when you come back?)


This Blog is about how to manage a popup window in a web page. I will present JavaScript functions that build, open and close it safely. You can find this also on my homepage. There are simpler solutions for popup / dropdown windows, but they do not work always and under all circumstances.

Example

Scroll to any "Popup X" and click it.
For simplicity, this popup shows on left mouse click, other than traditional context menus that show on right mouse click.


1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

Event Log:

Tests:

  1. Click a "Popup X".
    Expected: a white popup window opens.

  2. Click into the open popup window.
    Expected: the click target is reported in event log.

  3. Open a "Popup". Click anywhere but not in a "Popup" or a scrollbar.
    Expected: the open popup closes.

  4. Open a "Popup". Open another "Popup".
    Expected: first popup closes when second opens.

  5. Make browser page small. Open a "Popup" outside the blue scroll-pane. Scroll the browser page.
    Expected: the open popup closes.

  6. Open a "Popup" inside the blue scroll-pane. Scroll the pane.
    Expected: the open popup closes.

HTML and CSS

In this example, the popup is contained within its visual representation (the JS source below postulates that). The popup is tagged with CSS class popup. The visual representation renders just the popup's first text line. To do that, it was set to a fixed height, with CSS overflow hidden.

 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
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>JS Popup Context</title>

    <style>
      .popup-container
      {
        height: 1.2em; /* view just first line of nested popup */
        overflow: hidden; /* hides lower part of nested popup */
        
        /* following are not needed */
        width: 8em; /* arbitrary width */
        border: 1px solid gray; /* visual marker */
      }
      .popup
      {
        min-width: 8em; /* minimal width is same as container */
      }
    </style>

  </head>
  
  <body>

    <div class="popup-container">
      <div class="popup">
        <div>Popup 0</div>
        <div>Menu</div>
        <div>Items</div>
      </div>
    </div>

    <script>
      (function() {
        /* JS source goes here */
      })();
    </script>

  </body>

</html>

JS Source Code

The popup principles are following:

  1. there must be a visual representation of the popup, and a hidden list of items or whatever

  2. the popup needs to be cloned, because the original element might not display correctly in its parent's scope; the clone needs to be added to the document's body, hidden, and its CSS position must be absolute

  3. clicking onto the visual representation makes the hidden clone visible, and positions it to where the visual representation is; the JS function getBoundingClientRect() lets calculate this

  4. a click onto the popup will close it

  5. any click outside the popup must also close it

  6. any scrolling also should close the popup; if this is not wanted, the popup must move with the scrolling container

Put following functions to where /* JS source goes here */ is.

Script Initialization

This loops all popups and installs them. A click callback is added on each one, logging the event target. (In real world, you would replace this by some context menu event dispatcher.)

This source code needs to be on bottom of the script element, all other functions must be somewhere above it.

      var popups = document.querySelectorAll(".popup");
      for (var i = 0; i < popups.length; i++) {
        var popupClone = installPopup(popups[i]);
        
        popupClone.addEventListener("click", function(event) {
          var log = document.getElementById("log");
          var clickTarget = (event.target || event.srcElement);
          log.innerHTML = log.innerHTML+"<p>Click target was: '"+clickTarget.textContent+"'</p>";
        });
      }

Installation

This prepares the given popup for usage. It adds all necessary listeners to open and close it.

      var installPopup = function(popup, keepOpenOnScroll) {
        var popupClone = createPopupClone(popup);
        
        /* event listener that opens the popup */
        popup.addEventListener("click", function(event) {
          openPopup(popup, popupClone, event);
        });
        
        /* event listeners that close the popup */
        var close = function() {
          closePopup(popupClone);
        };
        popupClone.addEventListener("blur", close);
        popupClone.addEventListener("click", close);
        
        if ( ! keepOpenOnScroll ) {
          var scrollParent = findScrollParent(popup);
          if (scrollParent)
            scrollParent.addEventListener("scroll", close);
          
          window.addEventListener("scroll", close);
        }
        
        return popupClone;
      };

Create Invisible Popup as Clone

This clones the popup element and adds it invisibly to the document's body root.

      var createPopupClone = function(popup) {
        var popupClone = popup.cloneNode(true); /* create a deep clone */
        
        closePopup(popupClone); /* set it invisible */
        
        if ( ! popupClone.style["background-color"] )
          popupClone.style["background-color"] = "white"; /* don't be transparent */
          
        popupClone.style["position"] = "absolute"; /* position will be calculated */
        document.body.appendChild(popupClone); /* add to root level for absolute positioning */
        
        popupClone.setAttribute("tabindex", 0); /* make it focusable for blur-listener */
        
        return popupClone;
      };

Open and Close the Popup

The open-function calculates the absolute coordinates of the popup and positions it there. (This could be refined to calculate an actually visible rectangle on the screen when being on bottom. See the location() and smartCoordinate() functions in my tooltip Blog for improving this.)

The focus() call sets the input focus onto the popup clone. This is necessary for the blur listener to work. Setting the clone's tabindex attribute to 0 made it focusable (see createPopupClone() above).

      var openPopup = function(popup, popupClone, clickEvent) {
        var rectangle = popup.getBoundingClientRect();
        var scrollOffsetLeft = window.pageXOffset || document.documentElement.scrollLeft;
        var scrollOffsetTop  = window.pageYOffset || document.documentElement.scrollTop;
        popupClone.style.top = Math.round(rectangle.top + scrollOffsetTop)+"px";
        popupClone.style.left = Math.round(rectangle.left + scrollOffsetLeft)+"px";
        
        popupClone.style["display"] = "";
        popupClone.focus();
      };
        
      var closePopup = function(popupClone) {
        popupClone.style["display"] = "none";
      };

Find Parent Scroll Containers

These functions identify and find parent elements that show either an horizontal or a vertical scrollbar. A listener for these would use the "scroll" event type.

Mind that, in nested scroll panes, when you want to not close the popup on scrolling, the popup would not scroll with the parent. It would stay on its absolute coordinate. In that case you need to implement some kind of re-positioning on each scroll event.

      var isScrollPane = function(element) {
        var scrolls = (element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth);
        if ( ! scrolls )
          return false;
          
        /* H1 elements cause this to fail when not checking "overflow" */
        var style = window.getComputedStyle(element);
        return style.overflow === "scroll" || style.overflow === "auto";
      };
    
      var findScrollParent = function(element) {
        while (element.parentElement && element.parentElement !== document.body) {
          if ( isScrollPane(element.parentElement) )
            return element.parentElement;
            
          element = element.parentElement;
        }
        return undefined;
      };

Resume

Popups on web pages can be managed in many different ways. I tried to show a minimalistic and safe one, pointing out just the necessities.