Blog-Archiv

Samstag, 18. Juli 2020

HTML-5 Tree Code Generator

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!

Input
Tree Level Characters:
HTML
Output

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.

Cats
Garfield
Catbert

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.




Keine Kommentare: