Blog-Archiv

Dienstag, 31. März 2015

JS Treetable

Hierarchical thinking is deeply rooted in our brains. It is a simple and effective concept. Only in our fast-changing modern times relational thinking has become more important.

  • Hierarchical thinking is the old Roman "Divide et impera". Split a big problem into smaller ones and then try to solve the smaller problems. Parts of a hierarchy are exchangeable and reusable, especially when the association from parent to child has no back-link.

  • Relational thinking is more navigational. It views the problem from different aspects, then surveys and filters it until solutions become visible. More like surrounding the enemy and making it give up :-) Separating parts of a relational system always leaves open references.

To have the best of both worlds available on our computers, the Treetable component exists. It is a table with multiple columns, and in one of these columns, most likely the first, a tree is rendered. You could also regard a tree to be a treetable with just one column.

This article is about how to integrate a treetable into an HTML web page using JavaScript, without using jQuery or any JS Widget library. It is just about building the treetable, not about loading folders dynamically, or editing it via drag & drop.


The Beauty

Click on the black triangles below to collapse or expand the tree. Mind that columns do not resize when the tree is expanded or collapsed.

Item Chapter PI
Item 1 1 3.14159 26535
Item 2 1.1 3.14159 26535
Item 3 1.1.1 3.14159 26535
Item 4 1.1.2 3.14159 26535
Item 5 1.1.2.1 3.14159 26535
Item 6 1.1.2.2 3.14159 26535
Item 7 2 3.14159 26535
Item 8 2.1 3.14159 26535

Also test following behaviour:

  • collapse "Item 4"
  • collapse "Item 1"
  • expand "Item 1" again and check that "Item 4" is still collapsed
  • now expand "Item 4"
  • collapse "Item 1"
  • expand "Item 1" again and check that "Item 4" is still expanded

Here is the HTML source code for this treetable:

 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
<table data-tree-expanded="true" data-tree-column="0">
      <thead>
        <tr>
            <th>Item</th>
            <th>Chapter</th>
            <th>PI</th>
        </tr>
      </thead>
      <tbody>
        <tr data-tree-level="0">
            <td>Item 1</td>
            <td>1</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="1">
            <td>Item 2</td>
            <td>1.1</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="2">
            <td>Item 3</td>
            <td>1.1.1</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="2">
            <td>Item 4</td>
            <td>1.1.2</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="3">
            <td>Item 5</td>
            <td>1.1.2.1</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="3">
            <td>Item 6</td>
            <td>1.1.2.2</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="0">
            <td>Item 7</td>
            <td>2</td>
            <td>3.14159 26535</td>
        </tr>
        <tr data-tree-level="1">
            <td>Item 8</td>
            <td>2.1</td>
            <td>3.14159 26535</td>
        </tr>
      </tbody>
</table>

<script type="text/javascript">
  var table = document.getElementsByTagName("table")[0];
  treetableModule.initializeTable(table);
</script>

What kinds of treetable informations are in this table?

  • Initialization,
    • whether the tree should be initially expanded: data-tree-expanded="...",
    • and which column is to be the tree column: data-tree-column="..."
  • the 0-n tree level for each row: data-tree-level="..."

No ids are needed, no parent-child associations. But there is another necessity:

  • rows must be in a depth-first traversal order

Depth-first traversal order is the shape of the tree when it is fully expanded, so this is a quite natural thing.

How this HTML table is turned into an expanding treetable is subject of the JavaScript to be introduced now. The script has about 200 lines of code. It does not use any external JS library. I will introduce each part of it piece by piece, and you can find the full source code on bottom of this page.

Because it is so beautiful :-), here is the collapsed variant of above treetable, with the tree being in second column:

Item Chapter PI
Item 1 1 3.14159 26535
Item 2 1.1 3.14159 26535
Item 3 1.1.1 3.14159 26535
Item 4 1.1.2 3.14159 26535
Item 5 1.1.2.1 3.14159 26535
Item 6 1.1.2.2 3.14159 26535
Item 7 2 3.14159 26535
Item 8 2.1 3.14159 26535

The Beast

When we want to write a JavaScript that turns a table into a treetable, we face following problems:

  • set an expand control to every parent row into the column where the tree should reside
  • store the state whether a row is expanded or collapsed into that very row
  • calculate and realize the indentation that expresses the tree level of every row
  • determine the parent/child relations, i.e. find the child rows for each parent row
  • toggle the expand control to a collapse symbol on click, and reverse, and make rows visible or invisible on that event
  • avoid that expanding or collapsing rows resizes the tree column, i.e. keep that constant without hiding anything

Store collapsed/expanded state into rows

I chose CSS classes to store the state, because this also could be used for additional state-stylings. To find out whether a row is expanded I check its CSS classes to contain the class "expanded". To set it expanded, I add the CSS class "expanded", and at the same moment also change its expand control to the new state (that way the two always should be "in sync").

  var expandSymbol = "\u25B8"; // triangle right unicode
  var collapseSymbol = "\u25BE"; // triangle down unicode

  var EXPANDED_RE = /\bexpanded\b/;
  var COLLAPSED_RE = /\bcollapsed\b/;

  var isExpanded = function(element) {
    var expanded = EXPANDED_RE.test(element.className);
    var collapsed = COLLAPSED_RE.test(element.className);
    return expanded ? true : collapsed ? false : undefined;
  };

  var setExpanded = function(row, expand, treeCellColumn, expandSymbol, collapseSymbol) {
    var expandControl = row.children[treeCellColumn].children[0];
    if (expand === true) { // expand folder
      row.className = row.className.replace(COLLAPSED_RE, "");
      row.className += " expanded";
      expandControl.innerHTML = collapseSymbol;
    }
    else if (expand === false) { // collapse folder
      row.className = row.className.replace(EXPANDED_RE, "");
      row.className += " collapsed";
      expandControl.innerHTML = expandSymbol;
    }
    else { // leaf item
      row.className = row.className.replace(EXPANDED_RE, "");
      row.className = row.className.replace(COLLAPSED_RE, "");
      expandControl.innerHTML = "";
    }
  };

Both functions work with the JS property "className". The isExpanded() function could be shorter, but by testing for both states it works precise and returns undefined when there is no state at all.

The setExpanded() function removes the old state and then appends the new state. The regular expression EXPANDED_RE can be read as a word boundary, followed by "expanded", followed by another word boundary. Regular expression word boundaries \b are start of text, space, tab, newline, and end of text.
The setExpanded() function also sets a new expansion symbol into the expand-control of that row. We haven't yet created that expand-control.

Determine parent/child relations

This is the core part of the script. We need to dynamically determine the child rows of a parent row, and, in the same manner, which of the rows are parent rows. i.e. can be expanded or collapsed.

To do this we need the levels of the rows, which is given by the data-tree-level. So we just need to read that attribute and turn it into a number. Having (1) the level, and (2) the assumption that rows are in depth-first-traversal order, we can loop them and find out which of them are parent rows: those that have at least one follower row with a level higher than their own level.

We do this loop initially to set an expand-control into every row that is a parent, and a placeholder to all other rows (placeholder needed for tree-indentation). We do this also on every expand/collapse click to determine which rows must be set visible or invisible.

On collapse clicks we must set all children below the clicked row invisible, recursively.
On expand clicks we must set all direct children of the clicked row to visible, and also the children of all expanded children, recursively. We must not set the children of collapsed children visible. This is the reason why we need a stored expansion state in every row.

  var getLevel = function(row) {
    return parseInt(row.getAttribute("data-tree-level"), 10);
  };

  var isBelowExpandedParent = function(baseLevel, childLevel, parents, precedingItem) {
    var levelChange = childLevel - baseLevel - parents.length - 1;
    if (levelChange < 0)
      for (var i = 0; i > levelChange; i--)
        parents.pop();
    else if (levelChange === 1)
      parents.push(precedingItem);
    else if (levelChange > 1)
      throw "Treetable items are not in a depth-first order!";

    for (var i = 0; i < parents.length; i++)
      if ( ! isExpanded(parents[i]) )
        return false;

    return true;
  };

  var findSubTree = function(row, onCollapse) {
    var baseLevel = getLevel(row);
    var parent = row.parentElement;
    var collection = [];
    var inSubTree = false;
    var parents = [];
    var precedingItem;

    for (var i = 0; i < parent.children.length; i++) {
      var child = parent.children[i];
      if (child === row) { // before start of sub-tree
        inSubTree = true;
      }
      else if (inSubTree) {
        var childLevel = getLevel(child);
        if (childLevel > baseLevel) { // in sub-tree
          // on collapse all children are collected, recursively,
          // on expand only direct children or items below expanded parents,
          // not items below collapsed parents
          if (onCollapse ||
              childLevel === baseLevel + 1 ||
              isBelowExpandedParent(baseLevel, childLevel, parents, precedingItem))
            collection.push(child);

          precedingItem = child;
        }
        else { // end of sub-tree
          inSubTree = false;
        }
      }
    }
    return collection;
  };

Both isBelowExpandedParent() and findSubTree() functions are quite complicated. findSubTree() receives a row it must find a sub-tree for. A sub-tree includes all children recursively, not only direct children. It loops over all rows and sets a mark when it has found the given row. After that mark it collects children until the level is decreasing below the start-level.

To collect children for both expanding and collapsing case it considers the onCollapse flag. Is this true, it collects the entire sub-tree. Is this false, it collects only direct children, and such that are below a parent that is expanded. It leaves out the rows that are below a parent which is collapsed.

To keep track of the current parent in that loop, a sub-function has been coded. It receives all necessary state information from its caller function: the current level, the base level, a stack of parents and the previous row. It maintains the stack of parents by inserting or removing when level increases or decreases. Besides this function also checks the level progression and throws an error when the level increases by more than one (you can not see grand-children without passing the parent before).

With these functions we have the tree logic implemented.

Add listeners and implement expand/collapse callbacks

The "click" callback function must do two things: (1) set visible or invisible rows that have been found by findSubTree(), and (2) toggle the expand-control to visualize the new state.

  var toggleExpansion = function(row, isExpanded, treeCellColumn, expandSymbol, collapseSymbol) {
    if ( ! expandSymbol || ! collapseSymbol )
      throw "Lost symbols!";

    var subTree = findSubTree(row, isExpanded);
    for (var i = 0; i < subTree.length; i++)
      subTree[i].style.display = isExpanded ? "none" : "";

    if (subTree.length > 0)
      setExpanded(row, ! isExpanded, treeCellColumn, expandSymbol, collapseSymbol);
  };

  var addListener = function(expandControl, row, treeCellColumn, expandSymbol, collapseSymbol) {
    expandControl.addEventListener("click", function() {
      toggleExpansion(row, isExpanded(row), treeCellColumn, expandSymbol, collapseSymbol);
    });
  };

The addListener() function installs the callback into the browser and makes all given parameters available to that very callback. This is what is called "lambda" or "continuation": some variable values that will still be there, unchanged, whenever an event occurs. JavaScript provides that by default, in Java you would need to make all parameters final to achieve that.

The toggleExpansion() function then actually performs the state change. It finds the sub-tree of its given row, toggles its visibility state, and then exchanges the expand symbol and the current expansion state by calling the setExpanded() function defined above.

So now we solved most problem listed above. What remains to do is initialization, putting all together and bringing it to life.

Initialization

What is missing is inserting the expand-controls, and managing the tree column width. To do the first we loop all rows and simply prepend a newly created span element before the content of the td tree cell in that row. We then call toggleExpansion() on that row, which will set the state and the according symbol to the created expand control. Finally we add our callback listener to the "click" event of the expand-control.

Mind that the table is initially built like it was fully expanded, even when data-tree-expanded mandates that it should be collapsed. This is to measure the maximum column width of the tree column. To not provoke strange effects on the browser UI, the table is kept invisible while this is in progress. In a second initialization the collapsed state then gets established when needed.

  var invisiblyInitializeAndConfigureTable = function(
      treetable,
      treeCellColumn,
      initiallyExpanded)
  {
    var rows = treetable.getElementsByTagName("TR");
    var foundTreeCell = false;
    
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var treeCell = row.children[treeCellColumn];

      if (treeCell) { // could contain no cells at all
        foundTreeCell = true;
        var level = getLevel(row);
        var expandControl = document.createElement("span");
        expandControl.style.cssText =
            "display: inline-block; "+ // force browser to respect width
            "width: 1em; "+
            "margin-left: "+level+"em; "+ // tree level indentation in "m"
            "margin-right: 0.2em; "+
            "cursor: pointer;";
        treeCell.insertBefore(expandControl, treeCell.childNodes[0]);

        toggleExpansion(row, initiallyExpanded, treeCellColumn, expandSymbol, collapseSymbol);

        addListener(expandControl, row, treeCellColumn, expandSymbol, collapseSymbol);
      }
    }
    return foundTreeCell ? rows : undefined;
  };

  var initializeAndConfigureTable = function(treetable, initiallyExpanded, treeCellColumn) {
    treeCellColumn = (treeCellColumn && treeCellColumn >= 0) ? treeCellColumn : 0;
    initiallyExpanded = (initiallyExpanded !== undefined) ? initiallyExpanded : false;

    treetable.style.visibility = "hidden"; // hide table until it is ready

    var rows = invisiblyInitializeAndConfigureTable(
        treetable,
        treeCellColumn,
        false); // to find out column widths, expand all initially

    if ( ! rows )
      throw "Found not a single tree cell at column index (0-n): "+treeCellColumn;

    var treeColumnWidth = rows[0].children[treeCellColumn].clientWidth;

    if ( ! initiallyExpanded ) // set the initial collapsed state
      for (var i = 0; i < rows.length; i++)
        toggleExpansion(rows[i], ! initiallyExpanded, treeCellColumn, expandSymbol, collapseSymbol);

    for (var i = 0; i < rows.length; i++)
      rows[i].children[treeCellColumn].style.width = treeColumnWidth+"px";

    treetable.style.visibility = "visible"; // show table
  };

The invisiblyInitializeAndConfigureTable() function inserts expand controls, called by initializeAndConfigureTable that sets the initial expansion state and the maximum column width.

Expand controls are styled to display: inline-block; to make the browser respect their width. This is essential for the tree indentation, set by "margin-left: "+level+"em;".

The width of the tree column is taken from first row's clientWidth and transferred as pixels to each td that is a tree cell.

With these two initialization functions we are ready to build any treetable, found by class or by id, whatever. For completeness here is the function that reads the initialization attributes from the heading table element.

  var initializeTable = function(treetable) {
    var treeCellColumnString = treetable.getAttribute("data-tree-column");
    var treeCellColumn = treeCellColumnString ? parseInt(treeCellColumnString, 10) : 0;
    var initiallyExpanded = treetable.getAttribute("data-tree-expanded") === "true";
    initializeAndConfigureTable(treetable, initiallyExpanded, treeCellColumn);
  };

Mind how the default for data-tree-expanded is set to false by comparing it to the string "true". Any other value than "true" would generate a value of false. Normally you don't want to have a tree fully expanded.

Full Source

Here is the full source for quickly trying this out.

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

var treetableModule = (function()
{
  var that = {};

  var expandSymbol = "\u25B8"; // triangle right unicode
  var collapseSymbol = "\u25BE"; // triangle down unicode

  var EXPANDED_RE = /\bexpanded\b/;
  var COLLAPSED_RE = /\bcollapsed\b/;

  var isExpanded = function(element) {
    var expanded = EXPANDED_RE.test(element.className);
    var collapsed = COLLAPSED_RE.test(element.className);
    return expanded ? true : collapsed ? false : undefined;
  };

  var setExpanded = function(row, expand, treeCellColumn, expandSymbol, collapseSymbol) {
    var expandControl = row.children[treeCellColumn].children[0];
    if (expand === true) { // expand folder
      row.className = row.className.replace(COLLAPSED_RE, "");
      row.className += " expanded";
      expandControl.innerHTML = collapseSymbol;
    }
    else if (expand === false) { // collapse folder
      row.className = row.className.replace(EXPANDED_RE, "");
      row.className += " collapsed";
      expandControl.innerHTML = expandSymbol;
    }
    else { // leaf item
      row.className = row.className.replace(EXPANDED_RE, "");
      row.className = row.className.replace(COLLAPSED_RE, "");
      expandControl.innerHTML = "";
    }
  };

  var getLevel = function(row) {
    return parseInt(row.getAttribute("data-tree-level"), 10);
  };

  var isBelowExpandedParent = function(baseLevel, childLevel, parents, precedingItem) {
    var levelChange = childLevel - baseLevel - parents.length - 1;
    if (levelChange < 0)
      for (var i = 0; i > levelChange; i--)
        parents.pop();
    else if (levelChange === 1)
      parents.push(precedingItem);
    else if (levelChange > 1)
      throw "Treetable items are not in a depth-first order!";

    for (var i = 0; i < parents.length; i++)
      if ( ! isExpanded(parents[i]) )
        return false;

    return true;
  };

  var findSubTree = function(row, onCollapse) {
    var baseLevel = getLevel(row);
    var parent = row.parentElement;
    var collection = [];
    var inSubTree = false;
    var parents = [];
    var precedingItem;

    for (var i = 0; i < parent.children.length; i++) {
      var child = parent.children[i];
      if (child === row) { // before start of sub-tree
        inSubTree = true;
      }
      else if (inSubTree) {
        var childLevel = getLevel(child);
        if (childLevel > baseLevel) { // in sub-tree
          // on collapse all children are collected, recursively,
          // on expand only direct children or items below expanded parents,
          // not items below collapsed parents
          if (onCollapse ||
              childLevel === baseLevel + 1 ||
              isBelowExpandedParent(baseLevel, childLevel, parents, precedingItem))
            collection.push(child);

          precedingItem = child;
        }
        else { // end of sub-tree
          inSubTree = false;
        }
      }
    }
    return collection;
  };

  var toggleExpansion = function(row, isExpanded, treeCellColumn, expandSymbol, collapseSymbol) {
    if ( ! expandSymbol || ! collapseSymbol )
      throw "Lost symbols!";

    var subTree = findSubTree(row, isExpanded);
    for (var i = 0; i < subTree.length; i++)
      subTree[i].style.display = isExpanded ? "none" : "";

    if (subTree.length > 0)
      setExpanded(row, ! isExpanded, treeCellColumn, expandSymbol, collapseSymbol);
  };

  var addListener = function(expandControl, row, treeCellColumn, expandSymbol, collapseSymbol) {
    expandControl.addEventListener("click", function() {
      toggleExpansion(row, isExpanded(row), treeCellColumn, expandSymbol, collapseSymbol);
    });
  };

  var invisiblyInitializeAndConfigureTable = function(
      treetable,
      treeCellColumn,
      initiallyExpanded)
  {
    var rows = treetable.getElementsByTagName("TR");
    var foundTreeCell = false;
    
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var treeCell = row.children[treeCellColumn];

      if (treeCell) { // could contain no cells at all
        foundTreeCell = true;
        var level = getLevel(row);
        var expandControl = document.createElement("span");
        expandControl.style.cssText =
            "display: inline-block; "+ // force browser to respect width
            "width: 1em; "+
            "margin-left: "+level+"em; "+ // tree level indentation in "m"
            "margin-right: 0.2em; "+
            "cursor: pointer;";
        treeCell.insertBefore(expandControl, treeCell.childNodes[0]);

        toggleExpansion(row, initiallyExpanded, treeCellColumn, expandSymbol, collapseSymbol);

        addListener(expandControl, row, treeCellColumn, expandSymbol, collapseSymbol);
      }
    }
    return foundTreeCell ? rows : undefined;
  };

  var initializeAndConfigureTable = function(treetable, initiallyExpanded, treeCellColumn) {
    treeCellColumn = (treeCellColumn && treeCellColumn >= 0) ? treeCellColumn : 0;
    initiallyExpanded = (initiallyExpanded !== undefined) ? initiallyExpanded : false;

    treetable.style.visibility = "hidden"; // hide table until it is ready

    var rows = invisiblyInitializeAndConfigureTable(
        treetable,
        treeCellColumn,
        false); // to find out column widths, expand all initially

    if ( ! rows )
      throw "Found not a single tree cell at column index (0-n): "+treeCellColumn;

    var treeColumnWidth = rows[0].children[treeCellColumn].clientWidth;

    if ( ! initiallyExpanded ) // set the initial collapsed state
      for (var i = 0; i < rows.length; i++)
        toggleExpansion(rows[i], ! initiallyExpanded, treeCellColumn, expandSymbol, collapseSymbol);

    for (var i = 0; i < rows.length; i++)
      rows[i].children[treeCellColumn].style.width = treeColumnWidth+"px";

    treetable.style.visibility = "visible"; // show table
  };

  var initializeTable = function(treetable) {
    var treeCellColumnString = treetable.getAttribute("data-tree-column");
    var treeCellColumn = treeCellColumnString ? parseInt(treeCellColumnString, 10) : 0;
    var initiallyExpanded = treetable.getAttribute("data-tree-expanded") === "true";
    initializeAndConfigureTable(treetable, initiallyExpanded, treeCellColumn);
  };

  var initializeTables = function(treetables) {
    for (var i = 0; i < treetables.length; i++)
      initializeTable(treetables[i]);
  };

  var getClassTreetables = function() {
    return document.getElementsByClassName("treetable");
  };

  var initializeClassTreetables = function() {
    initializeTables(getClassTreetables());
  };

  var setExpandSymbols = function(expandSym, collapseSym) {
    if (expandSym)
      expandSymbol = expandSym; 
    if (collapseSym)
      collapseSymbol = collapseSym; 
  };

  that.initializeClassTreetables = initializeClassTreetables;
  that.initializeTables = initializeTables;
  that.initializeTable = initializeTable;
  that.initializeAndConfigureTable = initializeAndConfigureTable;
  that.getClassTreetables = getClassTreetables;
  that.setExpandSymbols = setExpandSymbols;

  return that;

}());

You can always visit my homepage for the current state of this utility.



Mittwoch, 25. März 2015

Remote Desktop from LINUX to WINDOWS

This Blog is about how to setup a graphical LINUX "Remote Desktop" for connecting to (and working on) a WINDOWS machine. For instance you want to connect yourself from your LINUX machine at home to your (running) WINDOWS job machine in town over the internet, because you need to try out something with InternetExplorer.

To achieve that, two pieces of software must be installed on your LINUX:

  • VPN
  • Remmina or some other graphical client that can communicate with the WINDOWS machine by talking RDP (remote desktop protocol)

In this tutorial the name of the VPN server in the remote network will be vpn.workflow.com, and the IP address of the target WINDOWS machine in that network will be 192.168.1.123. The name of the VPN login user will be fridell, and the name of the login user of the target WINDOWS machine will be franzen.

Here is a link to a similar tutorial.

VPN - Virtual Private Network

A virtual private network makes it possible that your machine is in a local network that actually is not local but remote. In other words, you start a VPN connection and then have the machines of the remote network with their local addresses (192.168.X.Y) in your own network.

There are several implementations of VPN:

For a WINDOWS machine you need the Microsoft PPTP. For a VPN connection you need the name of the VPN server (that must be available via internet), and you must have a login on that server.

On Ubuntu systems, a VPN client software should be pre-installed. So you only need to configure and start it. You won't need to be superuser for this.

Somewhere on what is the "taskbar" of your Ubuntu, there is a "system tray" icon that represents active network connections. When you left-click it, a menu should open, showing all possible connections. In that menu, an item "VPN Connections" should be present. (When not, go to the internet and find out how to install VPN client, this should be easy.)

You now must add a VPN connection of type PPTP. Use the "Edit" item in that menu to do that. In the upcoming dialog, click "Add". You will be asked for a connection type, choose PPTP. (When this is not present, you need to install VPN PPTP first, see above).

In the following dialog you must enter the connection data of the network you want to connect to, which includes

  • the name of the VPN server of that network (NOT the machine you will connect to finally!),
  • your user name and password,
  • and most likely the domain name of the WINDOWS network.

Do not yet close this dialog. Click on "Advanced" and

  • switch OFF the options "PAP", "CHAP", "EAP" (whatever that means)
  • also switch OFF "Allow BSD compression"
  • then switch ON the "Use Point-to-Point Encryption"

These setting might be network-specific, but for me nothing else worked. See also this page recommending these settings.

Commit and "Save" your configuration. You can close the network dialog now. After this you should see your new connection in the system tray network icon menu.

Click on the item to activate the connection. Mind that when you do this for the first time, a dialog will ask you for your password to access the "VPN secrets". The password you must enter here is the password of your local machine, not the one of the VPN server.

When the menu item has a hook now, it worked. When not, check user name, password and domain name for correctness.

When something goes wrong, you will have to perceive that reporting errors is not a strength of NetworkManager. But at least there is a command line interface for experimenting. You can open a terminal window and work with nmcli (network manager command line interface):

# list all connections
nmcli con

# start a connection, using the name of the VPN connection
nmcli con up id "Workflow VPN"

# stop a connection
nmcli con down id "Workflow VPN"

Mind that you don't need to be superuser to do this. When you do this as superuser, other problems might occur that are not related to the real one. (You definitely don't want to expose your password in some configuration file!)

When you succeed to run the VPN connection, you should be able to ping the machine you would like to connect to after:

ping 192.168.1.123

When not, you can try to understand the logging output of NetworkManager. On Ubuntu it appends to the file /var/log/syslog. In my case I actually found a trace of what was wrong:

tail -200 /var/log/syslog | egrep "auth|terminat|error" 

Mar 28 11:40:11 fricat pppd[3900]: MS-CHAP authentication failed: E=648 Password expired
Mar 28 11:40:11 fricat pppd[3900]: CHAP authentication failed
Mar 28 11:40:11 fricat pppd[3900]: Connection terminated.

Remmina - the LINUX Remote Desktop

This is a network tool that comes with SFTP and SSH protocols out of the box, but not with RDP (remote desktop protocol) what you need for WINDOWS. You must add the RDP plugin explicitly when you install it. When you forget this and install the plugin later, you need to quit the Remmina application explicitly, because it does not quit by default when you close it (and thus would not know about its new plugin then).

Go to your preferred software install tool, Synaptic or whatever, and search for "Remmina". You should find this easily without adding additional Ubuntu download repositories. Activate it for installation, then search for "remmina-plugin-rdp" and also activate it. Apply the installs.

What remains to do is configure Remmina to connect to your WINDOWS machine. You should be able to find it in your main menu after installation. Launch it and then click "Connection" - "New". Now you must configure the WINDOWS machine you want to connect to (other than with VPN where you configured the remote VPN server).

Choose RDP as protocol, then enter the IP address of the target machine and your login user and password. Most likely you will not need the WINDOWS domain name any more here.

After creating the connection, click "Connect" when having it selected. A window should open showing your remote WINDOWS desktop - yes, it is real :-)

It took me some hours to achieve my LINUX remote desktop. I did not know whether such is possible before. I hope I made life easier for all that would like to have the same. Thanks to all the eager LINUX programmers that write these applications!



Dienstag, 24. März 2015

JS Slide Show Aftermath

Because the ~ 500 lines of code for slideshow.js took much more time than I expected, I won't discuss the source in details (like I announced in the slide show of my last Blog). Instead I will try to show up the nuts and bolts I've come across when writing that script.

For a demo of that project you can always have a look at the slide show on my homepage. Within that show you also can find all sources.

Render HTML Elements in a Slide

This is not really difficult. I did it by copying the element into the slide:

1
2
3
4
5
6
      var displayInSlide = function(htmlElement) {
        slideframe.innerHTML = htmlElement.outerHTML; // copy the element into slide
        
        var slide = slideframe.children[0];
        scaleContents(slide);
      };

Setting the element's outerHTML to the innerHTML of the slide frame does the work. Care must be taken when having an HTMLCollection of slides retrieved by document.getElementsByClassName(). This could be a "live" collection that detects the creation of a new (copied) element with the according class, and then you would have another slide in your show :-) Best is to copy the list of slides into an array before starting the show.

For normal text slides you will want them to be centered, and with a bigger font. Both is done by following CSS:

.slideframe  {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 98%; 
  height: 98%; /* enable vertical centering */
  margin: auto; /* makes it centered, at least horizontally */
  overflow: auto;

  /* default font, overridable in slideshow.js */
  font-size: 130%;
  font-family: sans-serif;
}

It is the display: flex that does the centering work here. Supported only by new browsers, not IE-10.

Mind that only slides smaller than the display area will be centered. When you want a centering effect also on bigger slides, or even long-text slides, you need to do something like the following:

      var scaleText = function(slide) {
        var downsizeTextVerticalPercent = 80;
        var downsizeTextHorizontalPercent = 70;

        // to keep vertical centering, set width/height only when content bigger than frame
        // but this MUST be done, else long text would be cut on top
        if (slide.scrollHeight >= controls.slideframe.clientHeight * downsizeTextVerticalPercent / 100)
          slide.style.height = downsizeTextVerticalPercent+"%";

        if (slide.scrollWidth >= controls.slideframe.clientWidth * downsizeTextHorizontalPercent / 100)
          slide.style.width = downsizeTextHorizontalPercent+"%";
      };

Display Hyperlinks

In HTML there is a tag called OBJECT. This is a wildcard element that takes every kind of URL and tries its best to display that URL. When the URL points to a web page, it embeds a browser view into the current page, having its own scrollbar and event queue. You can not receive key presses or mouse clicks into that page by a JS event listener in parent page. It is like an IFRAME element.

This works quite well for all kinds of resources, even for videos, although InternetExplorer (once again) will do things different, especially when rendering JS resources in an OBJECT tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
      var displayHyperLink = function(hyperLink) {
        slideframe.innerHTML =
          "<object type='text/html' height='100%' width='98%' data='"+hyperLink+"'></object>";
      };

      var display = function() {
        var htmlElement = slidesQueue.getCurrent();

        var hyperLink = (htmlElement.href || htmlElement.src);
        var isImage = (htmlElement.tagName === "IMG");

        if (hyperLink && ! isImage)
          displayHyperLink(hyperLink);

        ....
      };

Scale Images

You don't want your beautiful photos to appear in a complete wrong aspect ratio. The web advices us to use ....

img {
  height: 100%;
  width: auto;
}

.... for images that should have a dynamic size but want to keep their proportion. But what if the 100% height would make parts of the image disappear because it's a landscape? Then you would like to define width to 100% and height to auto. I solved this using JS and CSS as follows:

.imageHeightTooBig  {
  height: 100%;
  width: auto;
}
.imageWidthTooBig  {
  width: 100%;
  height: auto;
}

      var scaleImage = function(slide) {
        // precondition is that image is loaded and displayed
        var width = slide.naturalWidth || slide.scrollWidth;
        var height = slide.naturalHeight || slide.scrollHeight;
        var widthTooBig = (width >= controls.slideframe.clientWidth);
        var heightTooBig = (height >= controls.slideframe.clientHeight);
        
        if (widthTooBig || heightTooBig) {
          var className = heightTooBig ? "imageHeightTooBig" : "imageWidthTooBig";
          if (slide.className.indexOf(className) < 0)
            slide.className += " "+className;
        }
      };

This code does nothing when neither height nor width of the image (slide) are too big. In any other case it finds out which of them is too big and applies the according CSS class to the element. When both are too big this would result in 100% height and auto width.

That way the image never should have a scrollbar, and its aspect ratio should be correct. When making the browser bigger and reloading the image you will have a bigger picture.

It is always better to have an image in original aspect-ratio and without scrollbars. To look deeper into it there should be some kind of zoom installed.

Lazy Loading Images

When having an slide show with a lot of high-resolution images you will wait a long time when loading that page. The browser would try to resolve all images as soon as the HTML has been rendered.

To avoid this you can remove the src attribute from the IMG tag and put the URL into the alt attribute (= alternative text) instead. There must be some JS logic then that sets the src attribute when the slide is displayed. This would look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<body>

  <h1>My Best Photos</h1>

  <img alt="hikes/IMG_4021.JPG"/>
  <img alt="hikes/IMG_4104.JPG"/>
  <img alt="hikes/IMG_4171.JPG"/>
  ....
  ....
  <!-- lots of photos ... -->

  
  <script type="text/javascript" src="slideshow.js">
  </script>
  
</body>

There is no src attributes on these IMG elements, thus the browser would not load anything. As soon as one of the elements is displayed, JS can set the src attribute and thus start the image loading. When it is fully loaded, the image can be scaled and centered.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
          if ( ! slide.src && slide.alt ) // lazy loading images have src in alt attribute
            slide.src = slide.alt;

          if (slide.complete) { // is loaded
            scaleImage(slide);
          }
          else { // wait until loaded
            var scaleImageLater = function() {
              slide.removeEventListener('load', scaleImageLater); // important!
              scaleImage(slide);
            };
            slide.addEventListener('load', scaleImageLater);
          }

Let Choose the Slide Number

I started to implement this with hash-tags (the part behind the '#' in browser address line). An URL of my slide show looks like this (for slide 25):

http://fritzthecat-blog.blogspot.co.at/2015/03/#25

Editing that number would take you to the desired slide, that was my intent.

I then detected that I needed to click the browser "Reload" button to actually load that slide, pressing the ENTER key was not enough (at least for Firefox). I introduced an "hashchange" event listener and started to experiment, but quickly cancelled all efforts then, because this caused strange effects, slides were skipped and similar.

Unfortunately it is not possible to get back from the slide show to the previous location by using the browser "Back" button. Which is really ugly. I will try to realize this with HTML-5 history API.

Working with AMD

AMD demands that you use no global variables. Every module X needed to do the work of a module Y should be defined as dependency of Y. It is then passed as parameter to the factory-function of Y by the AMD loader.
Other parameters that are not modules must be passed to the module-function that the factory-function returns. To have an imagination how that looks, here comes an example.

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

define( // AMD module definition

  [ // dependency parameters, path relative to AMD base-path
    "queue",
    "slideshow/slidecontrols"
  ],

  function( // factory function, parameters are parallel to dependencies
    queue,
    slidecontrols
  )
  {
    return function(maximum, imageListener) // module function
    // page-specific configurations go to the module function
    {
      var install = function() {
        ....
        queue.initialize(maximum);
        ....
        slidecontrols.initialize(imageListener);
        ....
      };

      install();

    }; // module function end

  } // module factory function end

); // define module end

This is the archetype you have to live with when applying AMD. The whole module definition is enclosed in a define() call. The first parameter to it is an array of module identifiers, the second a factory function that produces the module when called by the AMD loader. The AMD loader resolves the dependencies and passes their results as parameters to the factory function. The result of that factory call is taken to be the module, be it an object or a function.

Working with my personal AMD loader I had to write an AMD caller for each HTML page that uses JS. This caller is for providing parameters to the module function. Here is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
"use strict";

define( // AMD module definition

  [ // dependency parameters, path relative to AMD base-path
    "slideshow/slideshow.js"
  ],
  
  function(slideshow) // factory function
  {
    var useDownwardButton = false;
    var documentInitiallyVisible = false;

    slideshow(
        useDownwardButton,
        documentInitiallyVisible
    );
  } // factory function end

); // define module end

Here the AMD factory function is used to call the dependency module (slideshow) with some page-specific configuration parameters.

A particular "pain in the brain" are circular references. Assume that module A needs B, but B needs also some services by A. Such should be resolved by cutting out from module A the part that module B needs, putting it to a new module C. Module B can than be built by passing C, and A can be built by passing B and C.

Implementing with AMD it not so easy. The same with refactoring. Being already difficult in JS, this is even more difficult in an AMD environment.

The deeper you go into JS, the more you find out that it is a writeonly language :-(