Blog-Archiv

Montag, 28. März 2016

JS Responsive Breakpoint Notifications

Responsive web-sites use so-called "CSS media queries" to adapt dynamically to the screen dimension they are rendered on. These "media queries" define conditions about the screen, with subsequent blocks of CSS-rules to be applied when the condition matches. Conditions sound like "When the device's width is smaller than 768 pixels". In this condition, 768 pixels would be called a "breakpoint" (has nothing to do with a debugger-breakpoint!).

Such a layout adaption is not always an easy task. In many cases also JavaScript code must be executed. In this Blog I will introduce a short and simple solution for JS "breakpoint" notifications.

Listening for Resize

This browser window is currently

1280

pixels wide.

Try to resize your browser window, the above pixel amount will update each time you do it. Here is the way how to listen for browser resizes:

  <i><p>
  This browser window is currently
  </p>
  <blockquote>
    <span id="width-output"></span>
  </blockquote>
  <p>
  pixels wide.
  </p></i>

  <script type="text/javascript">
    var outputWidth = function() {
      var element = document.getElementById("width-output");
      element.innerHTML = ''+window.innerWidth;
    };
    
    outputWidth();
    
    window.addEventListener("resize", outputWidth);
  </script>

This won't work on older exotic browsers, but essentially window.addEventListener("resize", listenerFunction) is all you need on modern HTML-5 browsers.

Provide the API

How do I want to register and receive breakpoint notifications? I want to pass an arbitrary pixel amount to the API, and a function to execute when the browser is resized across that breakpoint. Something like function addBreakpointListener(breakpoint, listenerFunction) { ... }; should do it.

The second thing the API must provide is the resize-listener function to install to the browser window. This could also be done internally, but leaving the resize-listener installation to the caller allows it also to de-install it temporarily, or install it at a certain time.

Here is the outline of the module to implement:

    /**
     * JS screen-width breakpoint manager that notifies
     * when a breakpoint is hit.
     */
    var screenWidthEventNotifier = function()
    {
      var that = {};
 
      /** Install this to receive browser-resize events. */
      that.resizeListener = function() {
        ....
      };
 
      /** Call this to add listeners to certain breakpoints. */
      that.addBreakpointListener = function(breakpoint, listenerFunction) {
        ....
      };
 
      return that;
    };

This API can then be used like the following:

    // create a breakpoint-notifier
    var breakpointNotifier = screenWidthEventNotifier();
 
    // install breakpoint-notifier as resize-listener
    window.addEventListener("resize", breakpointNotifier.resizeListener);
 
 
    // example breakpoint-listener implementation
    var breakpoint1024Listener = function(breakpoint, currentBrowserWidth) {
      var output = document.getElementById("output");
      output.innerHTML +=
          "<p>Received breakpoint event "+breakpoint+
          " at browser width "+currentBrowserWidth+", "+new Date()+"</p>";
    };
 
    // add example implementation as listener
    breakpointNotifier.addBreakpointListener(1024, breakpoint1024Listener);

This implementation anticipates that somewhere in the HTML page there is an element with id = "output". Every breakpoint-event would append a paragraph to it.

Detecting and Notifying Breakpoints

I will need a map that contains breakpoints as keys, and lists of listener-functions as value. That way I can check for all breakpoints, at any resize-event, whether the related listeners have to be called.

For detecting that a breakpoint is trespassed I will need a field that holds the previous screen width. Using this I can check whether the breakpoint is between current and previous width.

For determining the current browser width I will use window.innerWidth. This works on all modern browsers.

So here is my API implementation:

 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
    var screenWidthEventNotifier = function()
    {
      var breakpoint2ListenerLists = {};
      
      var browserWindowWidth = function() {
        return window.innerWidth;
      }
 
      var fireEvent = function(breakpoint, currentWidth) {
        var listeners = breakpoint2ListenerLists[breakpoint];
        for (var i = 0; i < listeners.length; i++) {
          var listener = listeners[i];
          listener(breakpoint, currentWidth);
        }
      };
 
      var previousWidth = browserWindowWidth();
 
      var that = {};
 
      /** Install this to receive browser-resize events. */
      that.resizeListener = function() {
        var currentWidth = browserWindowWidth();
 
        for (var breakpoint in breakpoint2ListenerLists) {
          if (breakpoint2ListenerLists.hasOwnProperty(breakpoint)) {
            // JS converts every map key to a string, so convert it back to integer
            var intBreakpoint = window.parseInt(breakpoint);

            if (intBreakpoint != currentWidth &&
                   (previousWidth <= intBreakpoint && intBreakpoint < currentWidth ||
                    previousWidth >= intBreakpoint && intBreakpoint > currentWidth))
            {
              fireEvent(intBreakpoint, currentWidth);
            }
          }
        }

        previousWidth = currentWidth;
      };
 
      /** Call this to add listeners to certain breakpoints. */
      that.addBreakpointListener = function(breakpoint, listenerFunction) {
        var listenersForBreakpoint = breakpoint2ListenerLists[breakpoint];
        if ( ! listenersForBreakpoint ) {
          listenersForBreakpoint = [];
          breakpoint2ListenerLists[breakpoint] = listenersForBreakpoint;
        }
        listenersForBreakpoint.push(listenerFunction);
      };
 
      return that;
    };

The line var breakpoint2ListenerLists = {}; sets up my breakpoint-to-listener map. As you might know, in JS there is no difference between a map and an object.

For determining the browser's window width I use window.innerWidth. This is encapsulated to be used once-and-only-once on (1) initialize and (2) every resize-event.

The fireEvent() function is private, it is not visible outside the module. I do not want any code to call this function except the internal resize-listener. The implementation receives the breakpoint that was hit, and the current browser width, as parameters. It then gets the listener array for the breakpoint out of the map, and loops its listeners, passing them the breakpoint and the current width. That way a listener can decide whether it wants to work below or above the breakpoint, or on both sides.

The private previousWidth field always will hold the width of the previous resize-event. Initially I assign the current browser width to it.

After the private part, I allocate a return-object called that where I will put my public functions into.

The most complex part is the resize-event-dispatch in resizeListener(). Essentially this functions loops all breakpoints on every resize-event, and checks whether the breakpoint is hit by the current change.
Do not pay attention to breakpoint2ListenerLists.hasOwnProperty(breakpoint), this is "boiler-plate code" to circumvent JavaScript for-in-loop problems with maps.
Assuming that my breakpoint is at 1024, my previous width at 1023, and my current width at 1024, I will NOT fire an event. When my previous width is at 1024 then, and my current width at 1025 or more, I will fire an event. That way I avoid that the listener receives a currentWidth being the same as the browser width, and thus can not decide whether the browser is now bigger or smaller than the breakpoint.

The public addBreakpointListener() implementation uses the breakpoint as key, and puts an empty array as value when there is no value yet. It adds the given listener to the array under that breakpoint.

Example Breakpoint Listener

Here is the Live-Example. Try it out by resizing your browser window across 1024 pixels in both directions. The JS listener working here is the one from "Provide the API" chapter.

Currently your browser is 1280 pixels wide.

Example listener output:




Sonntag, 13. März 2016

JS / CSS Tabs

"Tab" is a very vague term. It could mean anything from a trailer to a bottle, a tag, a bill, a stripe ...
Even the computer-world gave it several meanings (press TAB to navigate tabs below by keyboard:-)

There is the TAB key on your keyboard that produces kind of space when used in a text editor, or navigates the user interface by setting the input focus to buttons, choices or text-fields (this is called "tabbing").

This Blog is about how you can get tabs on your web page. It uses tabs itself to give you the feeling of what expects you. You should browse through all tabs in their order when reading this document.

All JS source code shown here is written in pure JavaScript, no jQuery is used.

Term Definitions


TermSemanticWAI-ARIA role
tab-panelthe possibly initially invisible rectangle holding all contents of a tabtabpanel
tab-buttonthe UI-control where you click upon to get a tab-panel to foregroundtab
tabbed panethe top-level container, holding all tab-buttons and tabs-
tab-listthe list of tab-buttons on top of every tabbed panetablist
tabsconvenience term designating a tabbed pane-

Preliminary Considerations


Implementing tabs by pure CSS is not recommendable. See what CSS-Tricks tells about this. The biggest problems may be that

  1. HTML and CSS code have to be very tightly coupled, and
  2. tab-buttons are hyperlinks, so you can not prevent the browser from scrolling down to the link's target, which is not ergonomic (⇾ avoid jumping views).

Pure JavaScript tabs are also not recommendable. Everybody will want to style tabs differently. Be it rounded corners just on one side, or on both, or none at all, custom focus colors, ....

So tabs should be implemented using both CSS and JS.


Implementations

The remainder of this Blog introduces HTML, JS and CSS source-code for tabs.

HTML

Here is an example HTML structure for tabs, as suggested by WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications). All tab-buttons are on top.

<div class="tabbed-pane" style="padding: 1em; border: 1px solid gray;">
    
    <ul role="tablist">
        <li id="tab-1" role="tab" aria-controls="tab-panel-1" aria-selected="true">Keyboard Key</li><!--
     --><li id="tab-2" role="tab" aria-controls="tab-panel-2" aria-selected="false">Text Space</li><!--
     --><li id="tab-3" role="tab" aria-controls="tab-panel-3" aria-selected="false">User Interface Element</li>
    </ul>
      
    <p role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">
        There is the TAB key on your keyboard ....
    </p>

    <p role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2">
        There may be tab-spaces in the text ....
    </p>

    <p role="tabpanel" id="tab-panel-3" aria-labelledby="tab-3">
        The user interface could use tabs ....
    </p>
</div>

JS

Tab-Module Outline

Put this script tag on bottom of your HTML page body. Copy all subsequent functions (except the utility modules) to where the "...." is.

<script type="text/javascript">

    var tabModule = function(domUtil, keyboard)
    {
      var that = {};
      
      that.observedKeyEvent = "keydown";
      
      ....

      that.init = function(topElement) {
        initTabbedPanes(topElement);
      };
      
      return that;     
    };

    tabModule(domUtil(), keyboard()).init();

</script>

The trailing line tabModule(domUtil(), keyboard()).init(); installs the tab functionality.

Tab-Module Functions

All of the following go to the Tab-Module script to where the "...." is.

The finder functions outline how the module works:

  • first it finds all tabbed panes, for each tabbed pane
    • it finds all tab-panels, for each tab-panel
      • it finds the tab-button
      • every button together with its panel is packed into a JS object
    • all of these objects are collected into an array
    • that array describes exactly one tabbed pane, and it is the return of function initTabs(), which is among the initializers.
      /**
       * Assumes that the direct parent of an element
       * with role "tablist" is a tabbed pane.
       * @return a list of all tabbed panes below given topElement.
       */
      that.findTabbedPanes = function(topElement) {
        var tabLists = topElement.querySelectorAll("[role = 'tablist']");
        var result = [];
        for (var i = 0; i < tabLists.length; i++)
          result.push(tabLists[i].parentElement);
        return result;
      };
      
      /**
       * Uses domUtil to find all tab-panels within a tabbed-pane,
       * not including tab-panels of nested tabbed panes.
       * @return a list of tab-panels nested directly into given tabbed pane.
       */
      that.findTabPanels = function(tabbedPane) {
        return domUtil.findChildren(
            tabbedPane,
            function(childElement) {
              return childElement.getAttribute("role") === "tabpanel";
            }
        );
      };
      
      /**
       * @return the tab-button for given tab-panel below given tabbed pane,
       *    considering all WAI-ARIA variants of describing it.
       */
      that.findTabButton = function(tabPanel, tabbedPane) {
        var tabButtonId = tabPanel.getAttribute("aria-labelledby");
        var result = document.getElementById(tabButtonId);
        
        if ( ! result )
          result = tabbedPane.querySelector("[aria-controls = '"+tabPanel.id+"']");
        
        if ( ! result )
          throw "Could not find button for tab id="+tabPanel.id;
        
        return result;
      };
      
      /*
       * @return true when given tab is currently selected,
       *     i.e. its "aria-selected" attribute is "true".
       */
      that.isSelectedTab = function(tabButton, tabPanel, tabbedPane) {
        return tabButton.getAttribute("aria-selected") === "true";
      };

Utility Modules

Copy these into a <script type="text/javascript"> tag before the Tab-Module script.

Following DOM module enables me to find child elements, recursively, that comply with a criterion given by a JS function. When a child matches, its children are not iterated. That way I do not find a tab-panel nested in a tab-panel when looking for tab-panels.

    var domUtil = function()
    {
      var that = {};
    
      /**
       * Returns children found by given matchFunction, recursively,
       * but not going into elements where matchFunction is successful.
       * @param topElement required, the element where to start recursive search.
       * @param matchFunction required, elements for which this function returns true will be returned.
       * @return the array of found elements (not nodes).
       */
      that.findChildren = function(topElement, matchFunction, found) {
        found = found || [];

        for (var i = 0; i < topElement.children.length; i++) {
          var child = topElement.children[i];

          if (matchFunction(child))
            found.push(child);
          else
            that.findChildren(child, matchFunction, found);
        }
        return found;
      };

      return that;
    };

CSS

Put following CSS styles into a <style type="text/css"> tag in head of your HTML.

The tabbed pane styles are essential, you should not leave them out. They arrange the list of buttons to be horizontal. But of course you can customize the corner rounding.

      /* do not display hidden tab panels */
      [aria-hidden = 'true'] {
        display: none;
      }
      
      /* a tab-button below a tabbed pane (not below an accordion) */
      .tabbed-pane > [role = 'tablist'] > [role = 'tab'] {
        display: inline-block; /* horizontally arranged */
        border-radius: 0 1em 0 0; /* rounded top-right corner */
        margin-right: 0.16em; /* a little space to next button */
      }
      
      /* tab button bar */
      [role = 'tablist'] {
        list-style: none; /* removes bullets */
        margin: 0; /* removes indentation */
        padding: 0;
      }

Quite a lot of source code to achieve tabs!
Visit the newest state of this project on my homepage.




Sonntag, 6. März 2016

JS Table Layout Adjustment: DIV Tables

This is the last of my series about adjusting columns of nested tables. It answers the question


Tables can be built not only by HTML TABLE elements, but also by DIV elements when using specific CSS display styles. Here is an example:

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

  <style type="text/css">
    .table {
      display: table;
    }
    .thead {
      display: table-header-group;
    }
    .tbody {
      display: table-row-group;
    }
    .tr {
      display: table-row;
    }
    .td, .th {
      display: table-cell;
    }
  </style>
  
</head>

<body>

  <div class="table">

    <div class="thead">

      <div class="tr">
        <div class="th">Column 1</div>
        <div class="th">Column 2</div>
      </div>

    </div>

    <div class="tbody">

      <div class="tr">
        <div class="td">Content One</div>
        <div class="td">Content Two</div>
      </div>

      <div class="tr">
        <div class="td">Content Three</div>
        <div class="td">Content Four</div>
      </div>

    </div>

  </div>

</body>
</html>

This displays like a normal HTML TABLE. The only difference is that DIV tables do not support certain features like e.g. the colspan attribute, and do not have browser-defaults for padding, margin etc.


How can I reuse the JS code for TABLE elements to do the same for DIV tables?

By implementing new categorizer and adjuster modules, both extending their already provided abstractions.
As you may remember, I implemented modules with different responsibilities:

  • identifying column cells as abstractCategorizer
    • concrete implementation as tableColumnCategorizer
  • measuring and sizing columns as abstractLayoutAdjuster
    • concrete implementation as nestedTablesColumnAdjuster

They were finally built together to create the adjuster fitting to the requirements.

Now here is the new categorizer module implementation for DIV tables:

 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
    "use strict";

    /**
     * Concrete categorizer for HTML div table columns.
     */
    var divTableColumnCategorizer = function(CATEGORY_ATTRIBUTE_NAME)
    {
      var that = abstractCategorizer(CATEGORY_ATTRIBUTE_NAME);

      /** @return always 1 because DIVs do not support the colspan attribute. */
      that.getSpan = function(element) {
        return 1;
      };

      /**
       * @return true when given element should be categorized.
       *    This implementation returns true when element is
       *    DIV and its "display" CSS property is "table-cell".
       */
      that.isElementToCategorize = function(element) {
        if ( ! element.tagName === "DIV" )
          return false;

        var style = window.getComputedStyle(element);
        return style["display"] === "table-cell"; 
      };

      return that;

    };

This module extends abstractCategorizer. It then implements getSpan() to return 1 (does not support colspan), and isElementToCategorize() to identify table-cells of DIV tables.


Now I need to write an adjuster that can find nested DIV tables.

 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
    "use strict";

    /**
     * Concrete implementation of a layout-adjuster for HTML DIV-tables.
     * @param abstractLayoutAdjuster the layouter to extend.
     */
    var nestedDivTablesColumnAdjuster = function(categorizer, abstractLayoutAdjuster)
    {
      var findNestedDivTables = function(element, nestedDivTables) {
        nestedDivTables = (nestedDivTables !== undefined) ? nestedDivTables : [];

        var children = element.children;
        for (var i = 0; i < children.length; i++) {
          var child = children[i];
          if (child.tagName === "DIV") {
            var style = window.getComputedStyle(child);
            if (style["display"] === "table")
              nestedDivTables.push(child);
          }
          findNestedDivTables(child, nestedDivTables);
        }
        return nestedDivTables;
      };

      var that = nestedTablesColumnAdjuster(categorizer, abstractLayoutAdjuster);

      /** @return all nested DIV table elements below given one. */
      that.getNestedContainers = function(elementToLayout) {
        return findNestedDivTables(elementToLayout);
      };

      return that;

    };

This module extends its predecessor module nestedTablesColumnAdjuster. It overwrites the getNestedContainers() function to call the private findNestedDivTables() implementation. Unfortunately the JS built-in querySelector() function can not be used here, because there is no CSS selector that can target CSS styles like display: table. So we need to loop through children and read their styles.


Here comes the code to integrate the two new modules:

      var categorizer = divTableColumnCategorizer();
      var abstractAdjuster = abstractLayoutAdjuster(categorizer);
      var columnAdjuster = nestedDivTablesColumnAdjuster(categorizer, abstractAdjuster);
      columnAdjuster.init(tables);

Remember that I provided an abstract module that provides pre-defined columns widths. To be able to integrate that module into the build-chain, I needed to specify the abstractAdjuster parameter separately.

Of course also both the elastic-column and predefined-column-widths modules can be reused for DIV tables. Just the build chain changes:

      var categorizer = divTableColumnCategorizer();
      
      var abstractAdjuster = predefinedSizes ?
          abstractPredefinedSizeAdjuster(categorizer, predefinedSizes) :
          abstractLayoutAdjuster(categorizer);
  
      var adjuster = nestedDivTablesColumnAdjuster(categorizer, abstractAdjuster);
      
      var columnAdjuster = elasticColumn ?
          elasticColumnAdjuster(categorizer, adjuster, elasticColumn) :
          adjuster;
  
      columnAdjuster.init(tables);

That's all! You can go to my homepage to see this working.


The beauty of code reuse by inheritance is that you are mostly done by overwriting only a few things. All object-oriented languages provide inheritance. Although JS is not object-oriented, similar can be provided by using functional inheritance.

Generally JS provides several kinds of inheritance (would we have needed so much?). There are prototypal, classical, .... I wrote about this in one of my past Blogs.
I recommend functional inheritance, because it is easy to use and understand, and avoids the risks of the prototype chain and the this pointer, and does not require understanding the new operator. And private instance variables work fine.

Keep it simple, and you will win!