Blog-Archiv

Sonntag, 7. Februar 2016

JS Table Layout Adjustment: Sizing

This is the continuation of my last Blog about adjusting columns of nested tables. After I managed to categorize all cells and thus assign them to a column, I should be able to size them now.

Years ago browsers had quite different techniques to format tables. There was the special table-layout: fixed; CSS property that allowed to size cells. Nowadays browsers accept a fixed cell width even without that property, and when you set the width to both CSS width and min-width, the table layout also survives window resizes. Should you have tables that change their cell values "on the fly", it is also recommendable to set the max-width, and with it overflow-x: hidden; to avoid big contents hanging into other cells.

With colspan cells there is special problem. I decided to not touch such cells at all, and until now this worked in all cases. But a prerequisite for that is that all nested tables are stretched to 100% width, so that the colspan-cells will spread between their sized neighbor cells.

Abstract Layout Adjuster

From the tableColumnCategorizer module's init() call I get back a map of arrays, and every array represents all categorized elements of a certain nesting-level. I find the category of an element in its data-layout-category attribute.

So now I can iterate the map, starting from deepest nesting level, going up to zero. On every level I can calculate the maximum cell width for every category (visible column). Subsequently I can size all cells of a category to their maximum. To cope also with colspan cells, all nested tables are finally stretched to 100% width.

 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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    "use strict";

    /**
     * Usage: layoutAdjuster().init(arrayOfTables);
     * @param categorizer required, puts categories into cells.
     */
    var abstractLayoutAdjuster = function(categorizer)
    {
      var calculateMaximums = function(categorizedElements) {
        var sizeMap = {};

        for (var i = 0; i < categorizedElements.length; i++) {
          var element = categorizedElements[i];

          if (that.isElementToSize(element)) {
            var category = categorizer.getCategory(element);
            var size = that.getSize(element);
            if (size > (sizeMap[category] || 0))
              sizeMap[category] = size;
          }
        }
        return sizeMap;
      };

      var calculateAndSize = function(categorizedElements) {
        var sizeMap = calculateMaximums(categorizedElements);

        for (var i = 0; i < categorizedElements.length; i++) {
          var element = categorizedElements[i];

          if (that.isElementToSize(element)) {
            var category = categorizer.getCategory(element);
            that.setSize(element, sizeMap[category]);
          }
        }
      };

      var stretchNestedContainers = function(nestedContainers, elementToLayout) {
        for (var i = 0; i < nestedContainers.length; i++) {
          var nestedContainer = nestedContainers[i];
          that.stretchElement(nestedContainer);
          
          var parent = nestedContainer.parentElement; /* stretch all parents up until TD */
          while ( ! categorizer.getCategory(parent) && parent !== elementToLayout) {
            that.stretchElement(parent);
            parent = parent.parentElement;
          }
        }
      };

      var that = {};

      /* public functions */

      /**
       * Does the layout and returns a structure for post-processing.
       * @param elementsToLayout required, array of tables to adjust.
       * @returns array of objects, per input table, with properties
       *     "levelArraysMap" (map with key = level and value = cell array of that level,
       *     "maximumLevel" (= length of levelArraysMap - 1).
       */
      that.init = function(elementsToLayout) {
        var structuredElements = [];

        for (var i = 0; i < elementsToLayout.length; i++) {
          var elementToLayout = elementsToLayout[i];
          var levelArraysMap = categorizer.init(elementToLayout);

          /* find out maximum level in map */
          var maximumLevel = -1;
          for (var level in levelArraysMap)
            if (levelArraysMap.hasOwnProperty(level) && level > maximumLevel)
              maximumLevel = level;

          for (var level = maximumLevel; level >= 0; level--)
            calculateAndSize(levelArraysMap[level]);

          /* stretch all nested tables to 100% */
          stretchNestedContainers(that.getNestedContainers(elementToLayout), elementToLayout);

          structuredElements.push({ /* provide return */
            levelArraysMap: levelArraysMap,
            maximumLevel: maximumLevel
          });
        }

        return structuredElements;
      };

      return that;
    };

For optimal encapsulation the module is split into private (var xxx = ...") and public (that.xxx = ...") functions. Mind that functions that do not yet exist, like that.setSize, are called as if they would exist. JS makes this possible, but the function must be there at runtime.

The calculateMaximums() function receives an array of elements. It creates a map and uses the category of every element as key for it. For every category the maximum is calculated. The map is returned for further processing.

The calculateAndSize() function sets the calculated maximum sizes to all elements with the according category.

The stretchNestedContainers() function loops over given nested tables and sets 100% width to them. For each such table it makes sure that all parents up to the next categorized cell are also sized to 100%, else the 100% would make no sense. Note that stretchElement() is not implemented yet, because I do not yet decide to stretch the width, it could be the height too.

The init() function first calls the given categorizer for a map of level arrays. It then counts the levels and loops them bottom-up. After calling calculateAndSize() with all of them it makes sure that all nested tables and their parents are sized to 100% width. It then returns, per given table, an object containing the level-array-map and the maximum level of that table (yes, JS does not provide the size of a map in a property!).

This module contains nothing specific to HTML TABLE, not even the functions to get and set sizes. That way it could be also used for adjusting row heights (mind that I named the function setSize and not setWidth). A specific module deriving this one will decide how the work is to be done, in this case by reading the JS property clientWidth, and writing the CSS property width.

Concrete TABLE Columns Adjuster


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

    /**
     * Concrete column-adjuster for HTML TABLEs.
     */
    var nestedTablesColumnAdjuster = function(categorizer)
    {
      var that = abstractLayoutAdjuster(categorizer);

      var getPaddingsLeftRight = function(element) {
        var style = window.getComputedStyle(element);
        return window.parseInt(style["padding-left"]) + window.parseInt(style["padding-right"]);
      };

      /* public functions */

      /** @return true when given element has no colspan. */
      that.isElementToSize = function(element) {
        return categorizer.getSpan(element) <= 1;
      };

      /** @return the clientWidth of given element, which includes paddings. */
      that.getSize = function(element) {
        return element.clientWidth;
      };

      /**
       * Sets given size as CSS width to given element after subtracting paddings.
       * @param size the clientWidth to achieve (includes local paddings).
       */
      that.setSize = function(element, size) {
        size -= getPaddingsLeftRight(element);
        var cssWidth = size+"px";

        element.style["width"] = cssWidth;
        element.style["max-width"] = cssWidth;
        element.style["min-width"] = cssWidth;
        element.style["overflow-x"] = "hidden";
      };

      /** @return all nested TABLE elements below given one. */
      that.getNestedContainers = function(elementToLayout) {
        return elementToLayout.querySelectorAll("TABLE");
      };

      /** Concrete implementation of stretching an element to full size. */
      that.stretchElement = function(element) {
        element.style["max-width"] = "";
        element.style["min-width"] = "";
        element.style["width"] = "100%";
      };

      return that;
    };

This synchronizes the column widths of nested TABLE elements, whereby the given top-element needs not to be a TABLE, it could also be a DIV.

The getPaddingsLeftRight() function calculates the horizontal paddings of an element. This is needed because I set the CSS width from the read-only JS property clientWidth, and this includes paddings, but width does not.

The isElementToSize() implementation makes sure that only elements without colspan are sized.

The getSize() implementation uses the clientWidth of an element. This includes the element's padding.

The setSize() implementation turns the (maximum) clientWidth into a CSS width by subtracting horizontal paddings of the target element. Then it sets all three of width, min-width, max-width to the same pixel value. Additionally it prevents contents to be written into other cells by setting overflow to "hidden".

The getNestedContainers() function uses the querySelectorAll() element function to retrieve all TABLE elements from given parent.

The stretchElement() function resets min-width and max-width, and then sets width to 100%.

Apply Modules

To try this out, mark your test-TABLE with CSS class="layoutNestedTables" and write following initialization-JS into your test page. Of course you also will need all code from predecessor Blog.

  <script type="text/javascript">
    "use strict";

    var initLayout = function() {
      var tables = document.getElementsByClassName("layoutNestedTables");

      for (var i = 0; i < tables.length; i++) /* set tables invisible while layouting them */
        tables[i].style.visibility = "hidden";

      var categorizer = tableColumnCategorizer();
      var columnAdjuster = nestedTablesColumnAdjuster(categorizer);
      columnAdjuster.init(tables);

      for (var i = 0; i < tables.length; i++) /* set tables visible again */
        tables[i].style.visibility = "visible";
    };

    window.addEventListener("load", initLayout);

  </script>

This sets all tables to adjust to invisible. Then it allocates a categorizer and an adjuster and makes them work. Finally all tables are set visible again. This is for avoiding probably slow layout work done before the user's eye.


The remaining topics for layout of nested tables are

  1. creating one elastic column for 100% stretched tables,
  2. pre-defining the widths for certain columns,
  3. doing all that also for DIV-tables.

Hope I will soon find time to document that all.




Keine Kommentare: