To cope with something means to live together with something unknown and seemingly uncontrollable. We don't need to control everything, but we also don't want to suffer from consequences of incalculable risks.
One thing we can do when we have a problem is giving it a name. Practically that name should make it more comprehensible. When we can understand it, we can cope with it.
This is not a self-help-group-instruction. This is about a web-page layout-problem solved by giving names. Although these are different things, I would consider the naming-activity being more important in this than the layout-solution. Remember:
We can cope with things that got a name.
And it will be fine when it is a meaningful name.
Layout Problem
This Blog is about HTML tables nested into other tables. The nested tables are not related to each other, thus their column widths will be different. Such layout does not look good. The JavaScript to be introduced in the following tries to fix that. (Mind that you won't have this problem when you never embed tables into other tables.)
Here is an example of what we have to cope with:
Date |
|
|||||||||
---|---|---|---|---|---|---|---|---|---|---|
2016-02-05 |
|
|||||||||
2016-02-06 |
|
|||||||||
2016-02-07 |
|
In this example table, all tables
have a green border,
header cells
a blue,
data cells
a magenta.
Tables of nesting-level 1 are yellow.
Table cells that have a colspan
attribute are rendered orange.
Sure, all data are there and readable. But we can hardly associate the columns in the different nested tables to each other. For example, try to sum up all prices.
The fix won't be just for the eye, it'll be for avoiding human mistakes. When we talk about shape and content, we should be aware that we need them both and together. As is HTML.
Synopsis
The introduction of the JavaScript solving this layout problem will be divided into several Blogs. This one is the first, and it is about naming cells being logically (but sometimes not visually) below each other.
The second one will be about adjusting these cells to have same widths. The column-width will be the initial width of the widest cell in it.
The third Blog will be about script extensions to achieve an elastic column in a 100% stretched table, and how to do the same for a DIV table.
All JS scripts will be modules, and I always use functional inheritance to reuse JS code in a simple and safe manner. No jQuery is used here, nevertheless the code works even in IE-9.
For a more compound test page and full JS source code you can visit my homepage.
Naming the Unknown
When I want to size all cells of one logical column to the same width, I have to cope with table-cell elements being somewhere in a tree of HTML elements, most likely quite far away from each other. To be able to size them, I give names to them. My naming scheme is the well-known chapter numbering system: 1 for first chapter, 1.1 for first sub-chapter of first chapter, 1.1.2 for second sub-sub-chapter of first sub-chapter, and so on. I will call these names categories, because they won't be unique identifiers for cells, but more a category a cell falls into.
Transferring this numbering system to the cells of a table containing nested tables, I would give 1 to all cells in first column, 2 to all cells in second column, and so on. To all cells in first column of a table nested into a cell 2, I would give 2.1. Would there be another table in that cell, its first cell would have the category 2.1.1.
1 |
|
|||||||||||||||
1 |
|
As you see, all cells that must have same widths are named by the same category (dotted number). The count of dots in the category reflects the nesting level, and the numbers give the column order index.
You could immediately write some CSS now to set fixed widths to the columns categorized in that way. But, in my opinion, JS solutions are more sophisticated and reusable, so I will do it with JS.
Abstract Categorizer
As the table nesting is not restricted to any depth, the JS implementation should work recursive. Here is a JS module that categorizes a given table. It is called "abstract" because it does not know yet the nature of the HTML tags it should look for. It just traverses the HTML table and names elements. A concrete extension of that module will add functions that decide which HTML elements should be looked for.
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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | /** * Usage: elementCategorizer().init(arrayOfTables); * @param CATEGORY_ATTRIBUTE_NAME optional name of the attribute where to put the * category into, default is "data-layout-category". */ var abstractCategorizer = function(CATEGORY_ATTRIBUTE_NAME) { "use strict"; CATEGORY_ATTRIBUTE_NAME = CATEGORY_ATTRIBUTE_NAME || "data-layout-category"; var buildDottedNumber = function(topElement, element, index) { var dottedNumber = ""+(index + 1); /* start numbering at 1 */ var parent = element.parentElement; while (parent !== topElement) { /* search for parent's dotted number */ var parentCategory = that.getCategory(parent); if (parentCategory) return parentCategory+"."+dottedNumber; parent = parent.parentElement; } return dottedNumber; }; var categorizeElement = function(topElement, element, index, levelArraysMap) { var predecessor = (index > 0) ? element.parentElement.children[index - 1] : undefined; var span = that.getSpan(predecessor); if (span > 1) /* when predecessor has colspan, increment index */ index += (span - 1); var dottedNumber = buildDottedNumber(topElement, element, index); element.setAttribute(CATEGORY_ATTRIBUTE_NAME, dottedNumber); var level = dottedNumber.split(".").length - 1; if (levelArraysMap[level] === undefined) levelArraysMap[level] = []; levelArraysMap[level].push(element); return index; }; var categorize = function(topElement, element, index, levelArraysMap) { /* build categories top-down, to enable parent category retrieval. */ if (that.isElementToCategorize(element)) index = categorizeElement(topElement, element, index, levelArraysMap); var children = element.children; /* go recursive */ var childIndex = 0; for (var i = 0; i < children.length; i++) { if (that.isVisible(children[i])) { childIndex = categorize(topElement, children[i], childIndex, levelArraysMap); childIndex++; } } return index; }; 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 the category of given element, can be undefined. */ that.getCategory = function(element) { return element.getAttribute(CATEGORY_ATTRIBUTE_NAME); }; /** @return true when the categorized elements can be found below given one. */ that.containsCategorizedElements = function(element) { return element.querySelector("["+CATEGORY_ATTRIBUTE_NAME+"]") !== null; }; /** @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 true if given element's display style is different from "none". */ that.isVisible = function(element) { return element && window.getComputedStyle(element).display !== "none"; }; /** * Categorize elements below given topElement. * @param topElement required, the element where to categorize below. * @return map of arrays of elements found in topElement, * key is level 0-n, * value is array of categorized elements on that level. */ that.init = function(topElement) { if (topElement === undefined) throw "Need a top-element to categorize!"; var levelArraysMap = {}; categorize(topElement, topElement, 0, levelArraysMap); return levelArraysMap; }; return that; }; |
Mind that two functions are not yet implemented:
- that.isElementToCategorize(element)
- that.getSpan(predecessor)
The isElementToCategorize()
function will restrict the module to HTML TABLE elements with TH and TD cells.
But I want to apply that module also on tables built from DIV elements with CSS display: table
.
So I will implement concrete sub-classes of this, one for TABLE elements, one for DIV-table elements.
The colspan
attribute is specific to TABLE, a DIV-table doesn't support that.
The getSpan()
implementation for TABLE will return the number in the colspan
attribute,
or 1 when not found, and the DIV-table implementation always will return 1.
The
CATEGORY_ATTRIBUTE_NAME
parameter lets set a name for the element attribute where the name (category) will be written into.The
buildDottedNumber()
function receives the index of the element to name, and it ascends until it finds a parent-category. When it does not find one, the index alone will be the category, else it is appended to the parent-category.The
categorizeElement()
function recognizes thecolspan
of a predecessor element, and corrects the index when one exists. It sets the built category into the element attribute. Then it calculates the nesting level from the number of dots in the category, and adds the element to an array in a level-map which is to return.The
categorize()
function is the recursive traversal. First it categorizes the element received as parameter, then it calls itself recursively in a loop over all children (whereby the child-index will be part of the the generated category). Keeping that call-order, the children will always find parent-categories when callingbuildDottedNumber()
.The
getCategory()
function reads the category from an element. This is here to encapsulateCATEGORY_ATTRIBUTE_NAME
. Mind that this is the only public function here, besidesinit()
. No other function will need to be called or overridden from modules extending this one.The
init()
function at last returns a map of levels. In each level (0-n, used as map-key) an array of elements on that level is stored. This return-map can be used to layout the table.
Now I can extend this module to implement a concrete table-column categorizer.
Concrete TABLE Categorizer
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 | /** * Concrete categorizer for HTML table columns. */ var tableColumnCategorizer = function(CATEGORY_ATTRIBUTE_NAME) { "use strict"; var that = abstractCategorizer(CATEGORY_ATTRIBUTE_NAME); /* public functions */ /** * @return true when given element should be categorized. * This implementation returns true when element is * TD (table cell) or TH (table header). */ that.isElementToCategorize = function(element) { return element.tagName === "TD" || element.tagName === "TH"; }; /** @return the number of following elements the given one spans. */ that.getSpan = function(element) { var colspan = element ? element.getAttribute("colspan") : undefined; return colspan ? window.parseInt(colspan) : 1; }; return that; }; |
This adds the not-yet-implemented functions, and thus makes the abstract module concrete. The extension of the abstract module is done by the line
var that = abstractCategorizer(CATEGORY_ATTRIBUTE_NAME);
This is like extending a class in Java. Just that in JS no classes exist, everything is an object. As a consequence you can extend modules even at runtime, which is quite useful in some situations.
The
isElementToCategorize()
function determines that TD and TH elements will be named (categorized).The
getSpan()
function delivers the number of colums the given element spans. As I did not want to restrict this tocolspan
I named itgetSpan()
, because we also haverowspan
, and basically the script is also able to categorize cells for adjusting row heights.
That's it! Functional inheritance helps to keep JS modules short and encapsulated. When you start the concrete module over a TABLE, all cells will be categorized.
In my next Blog I will show how this can be used to adjust table columns.
Again there will be an abstractColumnAdjuster
with concrete sub-modules.
Keine Kommentare:
Kommentar veröffentlichen