HTML-5 Tree Code Generator
Published: 2020-07-18
Updated: 2020-07-26
Web: https://fritzthecat-blog.blogspot.com/2020/07/html-5-tree-code-generator.html
As you may have noticed, HTML-5 features an expand-control element called <details>
. The name suggests that there is something more which is not visible. In a passed Blog I showed how you can style such elements to build an expansible/collapsible tree. In this Blog I provide a utility to generate HTML-5 source code out of shorthand text trees.
Mind that Microsoft's Edge browser still doesn't support <details>
.
Create an HTML-5 Tree
In the "Input" panel below you see a dashed list that represents a tree structure. The dashes ('-') need to be at line start, but can be indented by spaces. One dash means root level, two dashes second level, and so on. Dashes can be replaced by plus-signs ('+'), representing an initially closed folder. Generally you can configure your own tree level characters. Try it out!
Triggering "Generate" will fill the "HTML" text-area with HTML-5 and CSS code, representing the tree according to the dashed list, and it also will render the tree in the "Output" panel.
You can modify the generated HTML/CSS code and view your changes by clicking "Display Changes". Mind that clicking "Generate" will always overwrite what is in "HTML" area.
CSS Technique
<style>
details.tree {
padding-left: 1em;
}
div.tree {
padding-left: 2.06em;
}
</style>
This CSS defines that any <details>
element of class tree
should be displayed one 'm'-width indented to the right below its parent element. The CSS em
is a font-dependent width, which anticipates that all class tree
elements need to have the same font size. That makes up the tree structure, recursively as trees are.
The tree leafs are modelled as DIV
, because these don't have any browser-specific margin or padding. They go to the right twice as they don't have the expand-control arrow on their left side.
<details class="tree"><summary>Cats</summary>
<div class="tree">Garfield</div>
<div class="tree">Catbert</div>
</details>
All you need to do is to classify your <details>
elements as tree
and nest them into each other. Leafs can be DIV
or any other block element, for example a source code listing. Just make sure that leafs not classified as tree
are enclosed in a DIV
with class tree
.
Generator JavaScript
For the interested, here is the JavaScript code of the generator. You don't need this for a <details>
tree!
JavaScript to convert a dashed text tree to HTML-5 code (click to expand).
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | /* Module function, reading input */ var buildTreeElements = function(inputTreeText, expandedLevelCharacter, collapsedLevelCharacter) { var getIndent = function(trimmedLine) { var expanded = true; var regExp = new RegExp("^\\"+expandedLevelCharacter+"*"); var level = regExp.exec(trimmedLine)[0].length; if (level <= 0) { regExp = new RegExp("^\\"+collapsedLevelCharacter+"*"); level = regExp.exec(trimmedLine)[0].length; expanded = false; } return { level: level, expanded: expanded }; }; var getTreeElement = function(lines, startLineIndex, precedingTreeElement) { var line = lines[startLineIndex].trim(); var indent = getIndent(line); if (indent.level <= 0) { /* text nested into previous tree element */ if (precedingTreeElement === undefined) throw "First tree element must start with '"+expandedLevelCharacter+"' or '"+collapsedLevelCharacter+"' character: '"+line+"'"; return getElementContent(lines, startLineIndex, precedingTreeElement.level + 1); } if (precedingTreeElement && indent.level > precedingTreeElement.level + 1) /* would have no parent */ throw "Tree level too deep: '"+line+"'"; line = line.slice(indent.level, line.length).trim(); return { level: indent.level, expanded: indent.expanded, content: line, endLineIndex: startLineIndex }; }; var getElementContent = function(lines, startLineIndex, level) { var LINE_SEPARATOR = "<br/>"; /* catenation character for multiple lines */ var multiLines = lines[startLineIndex].trim(); var i = startLineIndex + 1; var endLineIndex = i; for (var done = false; ! done && i < lines.length; i++) { var nextLine = lines[i].trim(); var indent = getIndent(nextLine); if (indent.level <= 0) { multiLines = multiLines + LINE_SEPARATOR + nextLine; endLineIndex = i; } else { /* next tree element detected */ done = true; endLineIndex = i - 1; /* un-read line */ } } return { level: level, expanded: undefined, content: multiLines, endLineIndex: endLineIndex }; }; var buildTreeElementsImpl = function() { var lines = inputTreeText.split(/\r?\n/); var treeElements = []; var index = 0; while (index < lines.length) { if (lines[index].trim()) { /* not empty */ var precedingTreeElement = (treeElements.length > 0) ? treeElements[treeElements.length - 1] : undefined; var treeElement = getTreeElement(lines, index, precedingTreeElement); treeElements.push(treeElement); index = treeElement.endLineIndex + 1; } else { index++; } } return treeElements; }; return buildTreeElementsImpl(); }; /* Module function, generating output */ var outputTree = function(treeElements, htmlPrintln, allElementsAreFolders, rootLevelBold, nonFoldersItalic) { var hasChildren = function(treeElements, i) { if (i >= treeElements.length - 1) return false; return treeElements[i].level < treeElements[i + 1].level; }; var printTree = function(treeElements) { var INITIAL_LEVEL = 1; /* minimal number of leading dashes */ var level = INITIAL_LEVEL; for (var i = 0; i < treeElements.length; i++) { var treeElement = treeElements[i]; for (; level > treeElement.level; level--) /* close preceding element */ htmlPrintln("</details>"); level = treeElement.level; var isFolder = hasChildren(treeElements, i); if (isFolder || (allElementsAreFolders && treeElement.expanded != undefined)) htmlPrintln( "<details "+ (treeElement.expanded ? "open" : "")+ " class=\"tree\"><summary>"+ treeElement.content+ "</summary>"+ (isFolder ? "" : "</details>")); else htmlPrintln( "<div class=\"tree\">"+ treeElement.content+ "</div>"); } for ( ; level > INITIAL_LEVEL; level--) htmlPrintln("</details>"); }; var printTreeStyles = function() { htmlPrintln("<style>"); htmlPrintln(" details.tree {"); htmlPrintln(" padding-left: 1em;"); if (rootLevelBold) htmlPrintln(" font-weight: bold;"); htmlPrintln(" }"); htmlPrintln(" div.tree {"); htmlPrintln(" padding-left: 2.06em;"); if (nonFoldersItalic) htmlPrintln(" font-style: italic;"); if (rootLevelBold) htmlPrintln(" font-weight: bold;"); htmlPrintln(" }"); if (rootLevelBold) { htmlPrintln(" details.tree details.tree, details.tree div.tree {"); htmlPrintln(" font-weight: normal;"); htmlPrintln(" }"); } htmlPrintln("</style>"); }; var printHtml5Tree = function() { htmlPrintln("<div>"); printTreeStyles(); printTree(treeElements); htmlPrintln("</div>"); }; printHtml5Tree(); }; /* Global variables */ var outputTextarea = document.getElementById("output-tree"); var sample = document.getElementById("sample"); /* "Generate" callback function */ var generate = function() { var inputTreeArea = document.getElementById("input-tree"); var inputTreeText = inputTreeArea.value.trim(); var expandedLevelCharacter = document.getElementById("expandedLevelCharacter").value; var collapsedLevelCharacter = document.getElementById("collapsedLevelCharacter").value; try { var treeElements = buildTreeElements(inputTreeText, expandedLevelCharacter, collapsedLevelCharacter); outputTextarea.value = ""; /* clear output */ var htmlPrintln = function(htmlText) { var currentText = outputTextarea.value; outputTextarea.value = (currentText ? currentText+"\n" : "")+htmlText }; var allElementsAreFolders = document.getElementById("allElementsAreFolders").checked; var rootLevelBold = document.getElementById("rootLevelBold").checked; var nonFoldersItalic = document.getElementById("nonFoldersItalic").checked; outputTree(treeElements, htmlPrintln, allElementsAreFolders, rootLevelBold, nonFoldersItalic); toHtml(); } catch (error) { alert(error); } }; /* "Display" callback function */ var toHtml = function() { sample.innerHTML = outputTextarea.value; }; /* "Copy" callback function */ var copyToClipboard = function() { document.getElementById('output-tree').select(); document.execCommand('copy'); };
|
Resume
Unfortunately <details>
elements are not animated. Implementing animated tree expansion with JavaScript/CSS is quite complex.
Another problem might be that it can't be configured whether a click on the <summary>
text should or should not open the details. Some users may prefer to click onto the text without opening the tree.
ɔ⃝ Fritz Ritzberger, 2020-07-18