JS Table Layout Adjustment: Elastic Column


Published: 2016-02-14
Updated: 2016-02-14
Web: https://fritzthecat-blog.blogspot.com/2016/02/js-table-layout-adjustment-elastic.html


This is the third part of the Layout-Adjustment for Nested Tables series, episode one. Today I'm gonna show you how ....

Stop it! Sorry, this is an IT-Blog, not a TV-series. It's different every time :-)

What I did not cover until now was having an ....

Elastic Column

To achieve an elastic column in an HTML table, I need to make all cells in that column 100% wide. Additionally I must make sure that all parents of those cells are also 100% wide, up until the top-table. Finally there will be an arbitrary number of columns that contain cells with fixed widths, and one column that shows an elastic behaviour.

Until now the JS code to build a layout-adjuster looked like this:

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

I will now introduce a new module that changes this build-code to the following.

      var categorizer = tableColumnCategorizer();
var columnAdjuster = nestedTablesColumnAdjuster(categorizer);
var elasticAdjuster = elasticColumnAdjuster(categorizer, columnAdjuster);
elasticAdjuster.init(tables, elasticColumnCategory);

Use that in the initLayout() function (see predecessor Blog) for trying out. The tables and elasticColumnCategory (e.g. "2.1") variables must be given as parameters. The elasticColumnAdjuster() function is the new module that makes it possible to define an elastic column. The elastic column will stretch when the table is 100% wide and the browser window widens.

Add Categorizer Functions

I need to identify all cells that are in the elastic column, or overlap it because they are either span-cells or parents of elastic cells. Columns are represented by categories, and cells have categories as attributes.

  1. I need a function that generates the next category for a given category, e.g. "1.2.4" for given "1.2.3". That way I could implement a loop that finds out whether a span-cell overlaps the elastic category. I will call that new function nextCategory(category).

  2. I need a function that tells me whether a category overlaps a given category because it is either the same category, or is a parent of the given category. I will call that new function startsWith(category), because "3.2" is a parent of "3.2.1".

Let me show the source code and then explain further. I will add to the abstractCategorizer module, because the new functions logically belong there.

    var abstractCategorizer = function(CATEGORY_ATTRIBUTE_NAME)
{
....

var that = {};

/* public functions */

/** @return the next after the given dotted number (category). */
that.nextCategory = function(dottedNumber) {
var head = "";
var tail = dottedNumber;
var lastDotIndex = dottedNumber.lastIndexOf(".");
if (lastDotIndex > 0) {
head = dottedNumber.substring(0, lastDotIndex + 1);
tail = dottedNumber.substring(lastDotIndex + 1);
}
return head+(window.parseInt(tail) + 1);
};

/** @return true when given criterion starts with startPart, considering dots. */
that.startsWith = function(criterion, startPart) {
if (criterion === startPart)
return true; /* equal */

if (criterion.length <= startPart.length)
return false; /* same length but not equal */

var part = criterion.substring(0, startPart.length);
if (part === startPart && criterion.charAt(startPart.length) === '.')
return true; /* excluded "1.22" matching "1.2" */

return false;
};

....

return that;
};

The nextCategory() function tries to find the rightmost dot in given category, fetch the number behind, increment it and return it with what was to the left of it. When it does not find a dot, it simply increments the given number.

The startsWith() function compares the given criterion and the startsWith part being in question. When they are equal, it returns true. If the criterion's length is shorter than or equal to the startsWith's length, it returns false. Finally it is sure that the criterion is longer than the startsWith part. Thus a sub-string of it can be compared to the startsWith part, and when they are equal and the criterion has a dot after, true is returned. Else it might have been criterion = "1.2.33" compared to startsWith = "1.2.3", and that criterion definitely is not parent of "1.2.3", thus false is returned.

I will use these new categorizer-functions in ....

The New Module

It extends a columnAdjuster module, in particular, it gets the module it must extend as parameter. That way it does not know whether it works on a real TABLE or a DIV-table, and will be reusable for both. In case TABLE we will pass a nestedTablesColumnAdjuster module instance to it.

Mind that this is a feature available in script languages only: inheritance at runtime!
In Java, which is a strongly typed compiled language, you could not extend another class at runtime, except when using byte-code-engineering libraries, but these are not part of the language, and the resulting source code is far from being readable.

Here is the new module.

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

/**
* Concrete implementation of a layout-adjuster that lets define one column as
* elastic (or "full-width"), which will stretch when the table is resized.
* This adjuster also stretches any given top-table to 100% of its parent.
* Usage:
* elasticColumnAdjuster().init(arrayOfTables, predefinedSizes);
*
* @param categorizer required, builds and manages dotted numbers (categories).
* @param columnAdjuster required, the adjuster implementation to extend.
*/
var elasticColumnAdjuster = function(categorizer, columnAdjuster)
{
var overlapsElastic = function(span, element, elasticCategory) {
var category = categorizer.getCategory(element);
while (span > 0) {
if (categorizer.startsWith(elasticCategory, category))
return true;

span--;
if (span > 0)
category = categorizer.nextCategory(category);
}
return false;
};

var fixSize = function(element) {
that.setSize(element, that.getSize(element));
};

var fixNonElasticStretchElastic = function(structuredElements, elasticCategory) {
for (var i = 0; i < structuredElements.length; i++) {
var levelArraysMap = structuredElements[i].levelArraysMap;
var maximumLevel = structuredElements[i].maximumLevel;

var elasticNonSpanCells = [];
var nonElasticSpanCells = [];

for (var level = 0; level <= maximumLevel; level++) {
var levelArray = levelArraysMap[level];

for (var cellIndex = 0; cellIndex < levelArray.length; cellIndex++) {
var cell = levelArray[cellIndex];
var span = categorizer.getSpan(cell);
var isElastic = overlapsElastic(span, cell, elasticCategory);

if ( ! isElastic && span > 1) /* fix non-elastic span cells */
nonElasticSpanCells.push(cell);
else if (isElastic && span <= 1) /* stretch elastic non-span cells */
elasticNonSpanCells.push(cell);
}
}

/* first fix cells ... */
for (var i = 0; i < nonElasticSpanCells.length; i++)
fixSize(nonElasticSpanCells[i]);

/* ... then stretch cells */
for (var i = 0; i < elasticNonSpanCells.length; i++)
that.stretchElement(elasticNonSpanCells[i]);
}
};

var that = columnAdjuster;

/* public functions */

var superInit = that.init;

/**
* Overridden to do post-processing:
* fix non-elastic span-cells, stretch elastic non-span cells,
* stretch all top-tables.
* @param elasticCategory the identifier, or dotted number (category),
* of the column to keep elastic.
* @param rest see columnAdjuster.
*/
that.init = function(elementsToLayout, elasticCategory) {
fixNonElasticStretchElastic(
superInit(elementsToLayout),
elasticCategory);

for (var i = 0; i < elementsToLayout.length; i++)
that.stretchElement(elementsToLayout[i]);
};

return that;

};

I will explain the short functions first, and do the fixNonElasticStretchElastic() at last, because it is not so easy to understand.

Inheritance is done by assigning the given columnAdjuster module to the local that variable. The override is done by replacing the init() function with a new implementation, after saving the old one to the local variable superInit.

    var elasticColumnAdjuster = function(categorizer, columnAdjuster)
{
....

var that = columnAdjuster;

var superInit = that.init;

that.init = function(elementsToLayout, elasticCategory) {
....

superInit(elementsToLayout),
....
}

return that;

}

The new init() implementation calls the fixNonElasticStretchElastic() function with what comes back from the superInit() call, and the category denoting the elastic column. Then it stretches all tables (received as parameter) to 100%.

The overlapsElastic() function finds out whether an element overlaps the elastic column. It also receives the span count of this element, and thus can also cover span-cells. For that purpose the categorizer.nextCategory() function is used.

The fixSize() function is used to fix an element to its current size. This is needed for span-cells that do not overlap the elastic column. Without fixing them, they also would be elastic.

Finally, the fixNonElasticStretchElastic() function does what its name announces a little fuzzy. It receives an array of top-level tables to be formatted. Every such table is represented by an object containing the maximumLevel and the levelArraysMap, where key is the 0-n nesting-level and value is an array of cells being on that level. For every top-table, the function then separates the cells overlapping the elastic column from those that do not. Only span-cells are added to the collection of non-elastic cells, and no span-cells are added to the collection of elastic cells. After that separation, the nonElasticSpanCells are fixed to their current width, and the elasticNonSpanCells are stretched to 100%.

Proof of Concept

Date
ProductPriceNote
2016-02-05
Chocolate for nothing! Was a gift
Bread $ 2.50, or was it $ 3.00 ?
2016-02-06
Vegetables $ 5.30 Bought on market
Fish $ 7.80 I do not eat salmon any more
Free afternoon, went for dinner with friends
2016-02-07
Fruit $ 2.60 Fresh and tasty

Mind that when the table was formatted once, and you change the elastic column and format is once more, the non-elastic columns will keep their sizes. Thus the table will get wider and wider. Do a browser page reload in between when trying out different elastic columns.



That's it, hope it works for you. When not, take a look at my homepage, there you'll find the current state of this project. Use Ctl-U to view source, the JS code is at bottom of the page.





ɔ⃝ Fritz Ritzberger, 2016-02-14