Introduce panel system for match information

Change-Id: Id209cb9d928f4511d02ade47543c3486a611313e
diff --git a/dev/js/spec/matchSpec.js b/dev/js/spec/matchSpec.js
index 5463835..42f40ad 100644
--- a/dev/js/spec/matchSpec.js
+++ b/dev/js/spec/matchSpec.js
@@ -350,9 +350,9 @@
       expect(e.classList.contains('active')).toBe(true);
       expect(e["_match"]).not.toBe(undefined);
 
-      actions = e.querySelector("p.ref > div.action.bottom").children;
+      actions = e.querySelector("p.ref > div.action.button-group").children;
       
-      expect(actions[0].getAttribute("class")).toEqual("meta");
+      expect(actions[0].getAttribute("class")).toEqual("metatable");
       expect(actions[1].getAttribute("class")).toEqual("info");
       expect(actions[2].getAttribute("class")).toEqual("tree");
       
@@ -366,7 +366,7 @@
       var e = matchElementFactory();
       var m = matchClass.create(e);
       m.open();
-      var relation = e.querySelector("p.ref > div.action.bottom > span:nth-of-type(3)");
+      var relation = e.querySelector("p.ref > div.action.button-group > span:nth-of-type(3)");
       expect(relation.getAttribute("class")).toEqual("tree");
       expect(document.getElementsByClassName("button-group-list").length).toEqual(0);
 
@@ -388,7 +388,7 @@
 
   });
 
-  describe('KorAP.MatchInfo', function () {
+  xdescribe('KorAP.MatchInfo', function () {
 
     var matchClass = require('match');
 
@@ -571,7 +571,7 @@
   });
 
 
-  describe('KorAP.MatchTable', function () {
+  xdescribe('KorAP.MatchTable', function () {
 
     var matchClass = require('match');
 
@@ -630,7 +630,7 @@
     });
   });
 
-  describe('KorAP.MatchTree', function () {
+  xdescribe('KorAP.MatchTree', function () {
     var tree;
     var matchClass = require('match');
 
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 8d566f3..0e9944d 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -250,13 +250,6 @@
           }
         }
       };
-
-      // Session has KQ visibility stored
-      /**
-       * TODO:
-      if (show["kq"])
-        showKQ.apply();
-      */
     };
 
     // There is more than 0 matches and there is a resultButton
@@ -266,14 +259,6 @@
         /**
          * Toggle the alignment (left <=> right)
          */
-        /*
-        resultButton.add(loc.TOGGLE_ALIGN, ['align','right','button-icon'], function (e) {
-          var ol = d.querySelector('#search > ol');
-          ol.toggleClass("align-left", "align-right");
-          this.toggleClass("left", "right");
-        });
-        */
-
         resultPanel.actions.add(loc.TOGGLE_ALIGN, ['align','right','button-icon'], function (e) {
           var ol = d.querySelector('#search > ol');
           ol.toggleClass("align-left", "align-right");
diff --git a/dev/js/src/loc/de.js b/dev/js/src/loc/de.js
index bcb75a1..a4085b2 100644
--- a/dev/js/src/loc/de.js
+++ b/dev/js/src/loc/de.js
@@ -25,10 +25,11 @@
   ];
 
   // Match view
-  loc.ADDTREE  = 'Relationen';
+  loc.ADDTREE   = 'Relationen';
   loc.SHOWANNO  = 'Token';
-  loc.SHOWINFO = 'Informationen';
-  loc.CLOSE    = 'Schließen';
+  loc.SHOWINFO  = 'Informationen';
+  loc.CLOSE     = 'Schließen';
+  loc.DOWNLOAD  = 'Herunterladen';
 
   loc.TOGGLE_ALIGN = 'tausche Textausrichtung';
   loc.SHOW_KQ      = 'zeige KoralQuery';
diff --git a/dev/js/src/match.js b/dev/js/src/match.js
index ad9a5b3..1a89a8f 100644
--- a/dev/js/src/match.js
+++ b/dev/js/src/match.js
@@ -10,20 +10,18 @@
  * - A click on a table field and a tree node should at the field description to the fragments list.
  */
 define([
-  'match/info',      // rename to anno
-  'match/treeitem',
   'buttongroup',
-  'buttongroup/menu',
+  'panel/match',
 	'util'
-], function (infoClass,treeItemClass,buttonGroupClass,buttonGroupMenuClass) { //, refClass) {
+], function (buttonGroupClass,matchPanelClass) { //, refClass) {
 
   // Localization values
   const loc   = KorAP.Locale;
-  loc.SHOWINFO  = loc.SHOWINFO  || 'Show information';
-  loc.ADDTREE   = loc.ADDTREE   || 'Relations';
-  loc.SHOWANNO  = loc.SHOWANNO  || 'Tokens';
+  // loc.SHOWINFO  = loc.SHOWINFO  || 'Show information';
+  // loc.ADDTREE   = loc.ADDTREE   || 'Relations';
+  // loc.SHOWANNO  = loc.SHOWANNO  || 'Tokens';
   loc.CLOSE     = loc.CLOSE     || 'Close';
-  loc.SHOW_META = loc.SHOW_META || 'Metadata';
+  // loc.SHOW_META = loc.SHOW_META || 'Metadata';
   
   // 'corpusID', 'docID', 'textID'
   const _matchTerms  = ['textSigle', 'matchID', 'available'];
@@ -50,6 +48,7 @@
      */
     _init : function (match) {
       this._element = null;
+      this._initialized = false;
 
       // No match defined
       if (arguments.length < 1 ||
@@ -164,89 +163,45 @@
       element.classList.add('active');
 
       // Already there
-      if (element.classList.contains('action'))
+      /*
+        if (element.classList.contains('action'))
         return true;
-
-      // Create action buttons
-      var ul = d.createElement('ul');
-      ul.classList.add('action', 'right');
-
-      element.appendChild(ul);
-      element.classList.add('action');
-
-      // Todo: Open in new frame
-
-      // Add close button
-      var close = d.createElement('li');
-      close.addE('span').addT(loc.CLOSE);
-      close.classList.add('close');
-      close.setAttribute('title', loc.CLOSE);
+      */
+      if (this._initialized)
+        return true;
       
+      var btn = buttonGroupClass.create(
+        ['action','button-view']
+      );
+
       var that = this;
+      btn.add(loc.CLOSE, ['button-icon','close'], function () {
+        that.close();
+      });
+      element.appendChild(btn.element());
 
-      // TODO:
-      //   Introduce panel object here!
-      
       // Add meta button
       var refLine = element.querySelector("p.ref");
 
       // No reference found
       if (!refLine)
         return;
-
-      var btns = buttonGroupClass.create(['action', 'bottom','button-panel']);
-
-      // Add meta button
-      btns.add(
-        loc.SHOW_META, ['meta'], function (e) {
-          that.info().showMeta();
-        }
-      );
-
-      // Add token annotation button
-      btns.add(
-        loc.SHOWANNO, ['info'], function (e) {
-          that.info().showTable();
-        }
-      );
-
-      // Add tree view button
-      btns.add(
-        loc.ADDTREE, ['tree'], function (e) {
-          if (KorAP.TreeMenu === undefined) {
-            KorAP.TreeMenu = buttonGroupMenuClass.create([], treeItemClass);
-            KorAP.TreeMenu.element().setAttribute('id', 'treeMenu');
-          };
-
-          var tm = KorAP.TreeMenu;
-
-          // Reread list
-          tm.info(that.info());
-          tm.readItems(that.treeMenuList());
-
-          // Reposition and show menu
-          tm.show();
-          tm.button(this);
-          tm.focus();
-        }
-      );
-
       
+      var panel = matchPanelClass.create(this);
+
+      this._element.insertBefore(
+        panel.element(),
+        this._element.querySelector("p.ref")
+      );
+
       // Insert before reference line
       refLine.insertBefore(
-        btns.element(),
+        panel.actions.element(),
         refLine.firstChild
       );
 
+      this._initialized = true;
       
-      // Close match
-      close.addEventListener('click', function (e) {
-        e.halt();
-        that.close()
-      });
-
-      ul.appendChild(close);
-
       return true;
     },
 
@@ -265,99 +220,6 @@
      */
     close : function () {
       this._element.classList.remove('active');
-      /*
-      if (this._info !== undefined) {
-        this._info.destroy();
-      };
-      */
-    },
-
-
-    /**
-     * Get and open associated match infos.
-     */
-    info : function () {
-
-      // TODO:
-      //   Rename info() to panel()
-
-      // Create match info
-      if (this._info === undefined)
-        this._info = infoClass.create(this);
-
-      // There is an element to append
-      if (this._element === undefined ||
-          this._element === null)
-        return this._info;
-      
-      // Info is already activated
-      if (this._info._element !== undefined)
-        return this._info;
-
-      var refLine = this._element.querySelector("p.ref");
-      this._element.insertBefore(
-        this._info.element(),
-        refLine
-      );
-      
-      return this._info;
-    },
-
-
-    // Return tree menu list
-    treeMenuList : function () {
-
-      if (this._menuList)
-        return this._menuList;
-
-      // Join spans and relations
-      var treeLayers = []
-      var spans = this.getSpans();
-      var rels = this.getRels();
-      var i;
-      for (i in spans) {
-        treeLayers.push(spans[i]);
-      };
-      for (i in rels) {
-        treeLayers.push(rels[i]);
-      };
-
-      // Get spans
-      treeLayers = treeLayers.sort(
-        function (a, b) {
-          if (a.foundry < b.foundry) {
-            return -1;
-          }
-          else if (a.foundry > b.foundry) {
-            return 1;
-          }
-          else if (a.layer < b.layer) {
-            return -1;
-          }
-          else if (a.layer > b.layer) {
-            return 1;
-          };
-          return 0;
-        });
-      
-      var menuList = [];
-      
-      // Show tree views
-      for (var i = 0; i < treeLayers.length; i++) {
-        var span = treeLayers[i];
-        
-        // Add foundry/layer to menu list
-        menuList.push([
-          span.foundry + '/' + span.layer,
-          span.foundry,
-          span.layer,
-          span.type
-        ]);
-      };
-
-      // Create tree menu
-      this._menuList = menuList;
-      return menuList;
     },
 
     
diff --git a/dev/js/src/match/info.js b/dev/js/src/match/info.js
deleted file mode 100644
index 655535f..0000000
--- a/dev/js/src/match/info.js
+++ /dev/null
@@ -1,430 +0,0 @@
-/**
- * Information about a match.
- */
-/*
- * TODO:
- *   Create a "panel" object, that is the parent of this
- *   class and supports a simple .add() method to add views
- *   to an element.
- */
-define([
-  'match/infolayer',
-  'match/table',
-  'match/treehierarchy',
-  'match/treearc',
-  'match/meta',
-  'util'
-], function (infoLayerClass,
-	           matchTableClass,
-	           matchTreeHierarchyClass,
-             matchTreeArcClass,
-	           matchMetaClass) {
-  
-  // Override 
-  KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () {
-    KorAP.log(0, 'KorAP.API.getMatchInfo() not implemented')
-    return {};
-  };
-
-  const loc = KorAP.Locale;
-  const d = document;
-
-  return {
-
-    /**
-     * Create new match object
-     */
-    create : function (match) {
-      return Object.create(this)._init(match);
-    },
-
-
-    /**
-     * Initialize object
-     */
-    _init : function (match) {
-      this._match = match;
-      this._visibleTable = false;
-      this._visibleMeta = false;
-      this.opened = false;
-      return this;
-    },
-
-
-    /**
-     * Get match object
-     */
-    match : function () {
-      return this._match;
-    },
-
-
-    /**
-     * Retrieve and parse snippet for table
-     * representation
-     */
-    getTableData : function (tokens, cb) {
-      var focus = [];
-
-      // Get all tokens
-      if (tokens === undefined) {
-        focus = this._match.getTokens();
-      } 
-
-      // Get only some tokens
-      else {
-        
-        // Push newly to focus array
-        for (var i = 0; i < tokens.length; i++) {
-          var term = tokens[i];
-          try {
-            // Create info layer objects
-            var layer = infoLayerClass.create(term);
-            layer.type = "tokens";
-            focus.push(layer);
-          }
-          catch (e) {
-            continue;
-          };
-        };
-      };
-      
-      // No tokens chosen
-      if (focus.length == 0)
-        cb(null);
-
-      try {
-        // Get info (may be cached)
-        KorAP.API.getMatchInfo(
-          this._match,
-          { 'spans' : false, 'layer' : focus },
-        
-          // Callback for retrieval
-          function (matchResponse) {
-
-            if (matchResponse === undefined)
-              cb(null);
-
-            // Get snippet from match info
-            if (matchResponse["snippet"] !== undefined) {
-              this._table = matchTableClass.create(matchResponse["snippet"]);
-              cb(this._table);
-            };
-          }.bind(this)
-        );
-      }
-      catch (e) {
-        KorAP.log(0, e);
-        cb(null);
-      };
-
-      /*
-      // Todo: Store the table as a hash of the focus
-      return null;
-      */
-    },
-
-
-    /**
-     * Receive meta data from server.
-     */
-    getMetaData : function (cb) {
-
-      var match = this._match;
-
-      try {
-        KorAP.API.getTextInfo(
-          match, {}, function (textResponse) {
-            
-            if (textResponse === undefined) {
-              cb(null);
-              return;
-            };
-
-            var doc = textResponse["document"];
-       
-            if (doc === undefined) {
-              cb(null);
-              return;
-            };
-
-            var fields = doc["fields"];
-            if (fields === undefined) {
-              cb(null);
-              return;
-            };
-
-            // Add metainfo to matchview
-            cb(matchMetaClass.create(
-              match, fields
-            ));
-          }
-        );
-      }
-      catch (e) {
-        KorAP.log(0, e);
-        cb(null);
-      };
-    },
-    
-
-    /**
-     * Retrieve and parse snippet for tree representation
-     */
-    getTreeData : function (foundry, layer, type, cb) {
-      var focus = [];
-
-      try {
-        // TODO: Support and cache multiple trees
-        KorAP.API.getMatchInfo(
-          this._match, {
-            'spans' : true,
-            'foundry' : foundry,
-            'layer' : layer
-          },
-          function (matchResponse) {
-            if (matchResponse === undefined) {
-              cb(null);
-              return;
-            };
-
-            // Get snippet from match info
-            if (matchResponse["snippet"] !== undefined) {
-              // Todo: This should be cached somehow
-
-              if (type === "spans") {
-                cb(matchTreeHierarchyClass.create(matchResponse["snippet"]));
-              }
-              else if (type === "rels") {
-                cb(matchTreeArcClass.create(matchResponse["snippet"]));              
-              }
-
-              // Unknown tree type
-              else {
-                cb(null);
-              };
-            }
-            else {
-              cb(null);
-            };
-          }.bind(this)
-        );
-      }
-      catch (e) {
-        KorAP.log(0, e);
-        cb(null);
-      };
-    },
-
-
-    /**
-     * Destroy this match information view.
-     */
-    destroy : function () {
-
-      // Remove circular reference
-      /*
-      if (this._treeMenu !== undefined)
-	      delete this._treeMenu["info"];
-      
-      this._treeMenu.destroy();
-      this._treeMenu = undefined;
-      */
-      this._match = undefined;
-      this._matchCreator = undefined;      
-      // Element destroy
-    },
-
-
-    /**
-     * Add a new tree view to the list
-     */
-    showTree : function (foundry, layer, type, cb) {
-      var matchtree = d.createElement('div');
-      matchtree.classList.add('matchtree', 'loading');
-
-      this.element().appendChild(matchtree);
-
-      // Add title line
-      var h6 = matchtree.addE('h6');
-      h6.addE('span').addT(foundry);
-      h6.addE('span').addT(layer);      
-
-      var tree = matchtree.addE('div');
-      
-      // Add close action button
-      var actions = this._addButton('close', matchtree, function (e) {
-        this.parentNode.removeChild(this);
-        e.halt();
-      });
-
-      // tree.classList.add('loading'); // alternatively
-
-      // Get tree data async
-      this.getTreeData(foundry, layer, type, function (treeObj) {
-        matchtree.classList.remove('loading');
-
-        // Something went wrong - probably log!!!
-
-        if (treeObj === null) {
-          tree.addT('No data available.');
-        }
-        else {
-          tree.appendChild(treeObj.element());
-          treeObj.show();
-
-          // Reposition the view to the center
-          // (This may in a future release be a reposition
-          // to move the root to the actual match)
-
-          // This is currently not supported by relations
-          if (type === "spans") {
-            var dl = d.createElement('li');
-            dl.className = 'download';
-            dl.addEventListener(
-              'click', function (e) {
-                var a = treeObj.downloadLink();
-                d.body.appendChild(a);
-                a.click();
-                d.body.removeChild(a)
-                e.halt();
-              }
-            );
-            
-            actions.appendChild(dl);
-          };
-          
-          treeObj.center();
-        };
-  
-        if (cb !== undefined)
-          cb(treeObj);
-      });
-      matchtree.classList.remove('loading');
-    },
-
-
-    // Add meta information to match
-    showMeta : function () {
-
-      // Already visible
-      if (this._visibleMeta)
-        return;
-
-      this._visibleMeta = true;
-
-      var metaTable = document.createElement('div');
-      metaTable.classList.add('metatable', 'loading');
-      this.element().appendChild(metaTable);
-
-      /*
-       * This was temporary
-      var metaInfo = this._match.element().getAttribute('data-info');
-      if (metaInfo)
-        metaInfo = JSON.parse(metaInfo);
-      */
-      var that = this;
-
-      this.getMetaData(function (meta) {
-
-        if (meta === null)
-          return;
-
-        // Load data
-        metaTable.classList.remove('loading');
-
-        metaTable.appendChild(meta.element());
-
-        // Add button
-        that._addButton('close', metaTable, function (e) {
-          this.parentNode.removeChild(this);
-          that._visibleMeta = false;
-          e.halt();
-        });
-      });
-
-      // Do not load any longer
-      metaTable.classList.remove('loading');
-    },
-
-
-    // Add table
-    showTable : function () {
-
-      // Already visible
-      if (this._visibleTable)
-        return;
-      
-      this._visibleTable = true;
-
-      // Append default table
-      var matchtable = d.createElement('div');
-      matchtable.classList.add('matchtable', 'loading');
-      var info = this.element();
-      info.appendChild(matchtable);
-
-      var that = this;
-
-      // TODO:
-      //   Create try-catch-exception-handling
-      
-      // Create the table asynchronous
-      this.getTableData(undefined, function (table) {
-
-        // Load data
-        matchtable.classList.remove('loading');
-
-        if (table !== null) {
-          matchtable.appendChild(table.element());
-	      };
-      });
-
-      // Add button
-      this._addButton('close', matchtable, function (e) {
-        this.parentNode.removeChild(this);
-        that._visibleTable = false;
-        e.halt();
-      });
-
-      // Load data
-      matchtable.classList.remove('loading');
-    },
-
-    // Add action button
-    // TODO:
-    //   These are view-buttons that should be added to
-    //   the view and not to the panel!
-    _addButton : function (buttonType, element, cb) {
-      // TODO: Unless existent
-      var actions = document.createElement('ul');
-      actions.classList.add('action', 'image');
-      var b = actions.addE('li');
-      b.className = buttonType;
-      b.addE('span').addT(buttonType);
-      b.addEventListener(
-        'click', cb.bind(element)
-      );
-
-      element.appendChild(actions);
-      return actions;
-    },
-
-
-    /**
-     * Create match information view.
-     */
-    element : function () {
-      
-      if (this._element !== undefined)
-        return this._element;
-      
-      // Create info table
-      var info = d.createElement('div');
-      info.classList.add('matchinfo');
-      
-      this._element = info;
-
-      return this._element;
-    }
-  };
-});
diff --git a/dev/js/src/match/treearc.js b/dev/js/src/match/treearc.js
index badbacf..6f0690b 100644
--- a/dev/js/src/match/treearc.js
+++ b/dev/js/src/match/treearc.js
@@ -404,6 +404,10 @@
       labelE.appendChild(textNode);

 

       var labelBox   = labelE.getBBox();

+

+      if (!labelBox)

+        console.log("----");

+      

       var textWidth  = labelBox.width; // labelE.getComputedTextLength();

       var textHeight = labelBox.height; // labelE.getComputedTextLength();

 

diff --git a/dev/js/src/panel.js b/dev/js/src/panel.js
index 9d7968f..acdf6d8 100644
--- a/dev/js/src/panel.js
+++ b/dev/js/src/panel.js
@@ -85,6 +85,9 @@
         view.element()
       );
 
+      if (view.afterEmbed)
+        view.afterEmbed();
+      
       view.panel = this;
     },
 
@@ -98,5 +101,18 @@
         }
       }
     },
+
+    /**
+     * Upgrade this object to another object,
+     * while private data stays intact.
+     *
+     * @param {Object] An object with properties.
+     */
+    upgradeTo : function (props) {
+      for (var prop in props) {
+        this[prop] = props[prop];
+      };
+      return this;
+    }
   }
 });
diff --git a/dev/js/src/panel/match.js b/dev/js/src/panel/match.js
new file mode 100644
index 0000000..aba8873
--- /dev/null
+++ b/dev/js/src/panel/match.js
@@ -0,0 +1,178 @@
+/**
+ * Define a panel for matches
+ */
+
+define([
+  'panel',
+  'match/treeitem',
+  'view/match/tokentable',
+  'view/match/meta',
+  'view/match/relations',
+  'buttongroup/menu',
+], function (panelClass,treeItemClass,tableView,metaView,relationsView,buttonGroupMenuClass) {
+
+  // Override 
+  KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () {
+    KorAP.log(0, 'KorAP.API.getMatchInfo() not implemented')
+    return {};
+  };
+
+  const loc = KorAP.Locale;
+
+  loc.SHOWANNO  = loc.SHOWANNO  || 'Tokens';
+  loc.SHOW_META = loc.SHOW_META || 'Metadata';
+  loc.ADDTREE   = loc.ADDTREE   || 'Relations';
+
+  return {
+    create : function (match) {
+      return Object.create(panelClass)._init(['matchinfo']).upgradeTo(this)._init(match);
+    },
+
+    // Initialize match panel
+    _init : function (match) {
+
+      this._match = match;
+
+      var a = this.actions;
+
+      // Ugly hack!
+      var cl= a.element().classList;
+      cl.remove('matchinfo');
+      cl.add('button-matchinfo');
+
+      // Add meta button
+      a.add(
+        loc.SHOW_META, ['metatable'], function (e) {
+          this.addMeta();
+        }
+      );
+
+      // Add token annotation button
+      a.add(
+        loc.SHOWANNO, ['info'], function (e) {
+          this.addTable();
+        }
+      );
+
+      // Add relations button
+      a.add(
+        loc.ADDTREE, ['tree'], function (e) {
+
+          // Get global tree menu
+          if (KorAP.TreeMenu === undefined) {
+            KorAP.TreeMenu = buttonGroupMenuClass.create([], treeItemClass);
+            KorAP.TreeMenu.element().setAttribute('id', 'treeMenu');
+          };
+
+          var tm = KorAP.TreeMenu;
+
+          // Set panel
+          tm.info(this);
+
+          // Reread list
+          tm.readItems(this._treeMenuList());
+
+          // Reposition and show menu
+          tm.show();
+          tm.button(this.button);
+          tm.focus();
+        }
+      )
+
+      return this;
+    },
+
+    
+    /**
+     * Add meta view to panel
+     */
+    addMeta : function () {
+      if (this._metaView && this._metaView.shown())
+        return;
+      this._metaView = metaView.create(this._match);
+      this.add(this._metaView);
+    },
+
+    
+    /**
+     * Add table view to panel
+     */
+    addTable : function () {
+      if (this._tableView && this._tableView.shown())
+        return;
+      this._tableView = tableView.create(this._match);
+      this.add(this._tableView);
+    },
+
+    /**
+     * Add Tree view to panel
+     */
+    // TODO: Rename to addTree() - this is called in treeitem.js
+    showTree : function (foundry, layer, type) {
+      this.add(
+        relationsView.create(this._match, foundry, layer, type)
+      );
+      
+    },
+
+
+    // Return tree menu list
+    _treeMenuList : function () {
+     
+      if (this._menuList)
+        return this._menuList;
+
+      var match = this._match;
+
+      // Join spans and relations
+      var treeLayers = []
+      var spans = match.getSpans();
+      var rels = match.getRels();
+      
+      var i;
+      for (i in spans) {
+        treeLayers.push(spans[i]);
+      };
+      for (i in rels) {
+        treeLayers.push(rels[i]);
+      };
+
+      // Get spans
+      treeLayers = treeLayers.sort(
+        function (a, b) {
+          if (a.foundry < b.foundry) {
+            return -1;
+          }
+          else if (a.foundry > b.foundry) {
+            return 1;
+          }
+          else if (a.layer < b.layer) {
+            return -1;
+          }
+          else if (a.layer > b.layer) {
+            return 1;
+          };
+          return 0;
+        });
+      
+      var menuList = [];
+      
+      // Show tree views
+      for (var i = 0; i < treeLayers.length; i++) {
+        var span = treeLayers[i];
+        
+        // Add foundry/layer to menu list
+        menuList.push([
+          span.foundry + '/' + span.layer,
+          span.foundry,
+          span.layer,
+          span.type
+        ]);
+      };
+
+      // Create tree menu
+      this._menuList = menuList;
+      return menuList;
+    }   
+  }
+});
diff --git a/dev/js/src/view.js b/dev/js/src/view.js
index 946cdd3..0a47f0b 100644
--- a/dev/js/src/view.js
+++ b/dev/js/src/view.js
@@ -5,6 +5,10 @@
 
 define(['buttongroup', 'util'], function (buttonGroupClass) {
 
+  const loc   = KorAP.Locale;
+  loc.CLOSE     = loc.CLOSE     || 'Close';
+  
+
   return {
     create : function (classes) {
       return Object.create(this)._init(classes);
@@ -19,11 +23,11 @@
       // The buttonclass is bind to the view
       var c = ['action', 'button-view'];
       if (classes)
-        c.push.apply(null,classes);
+        c.push.apply(c,classes);
       
       this.actions = buttonGroupClass.create(c).bind(this);
 
-      this.actions.add('close', ['button-icon','close'], function (e) {
+      this.actions.add(loc.CLOSE, ['button-icon','close'], function (e) {
         this.close();
       });
 
diff --git a/dev/js/src/view/match/meta.js b/dev/js/src/view/match/meta.js
new file mode 100644
index 0000000..f54c7fb
--- /dev/null
+++ b/dev/js/src/view/match/meta.js
@@ -0,0 +1,105 @@
+define([
+  'view',
+  'match/meta'
+], function (viewClass, matchMetaClass) {
+
+  const d = document;
+  
+  return {
+    create : function (match) {
+      return Object.create(viewClass)._init(['metatable']).upgradeTo(this)._init(match);
+    },
+
+
+    _init : function (match) {
+      this._match = match;
+      return this;
+    },
+    
+
+    /**
+     * Meta view element
+     */
+    show : function () {
+      if (this._show)
+        return this._show;
+
+      var metaTable = document.createElement('div');
+      metaTable.classList.add('metatable', 'loading');
+
+      this.getData(function (meta) {
+
+        if (meta === null)
+          return;
+
+        // Load data
+        metaTable.classList.remove('loading');
+
+        metaTable.appendChild(meta.element());
+      });
+
+      // TODO:
+      //   Loading should have a timeout on view-level
+      //   matchtable.classList.remove('loading');
+
+      this._show = metaTable;
+      return metaTable;
+    },
+
+
+    /**
+     * Get match object
+     */
+    match : function () {
+      return this._match;
+    },
+
+
+    /**
+     * Retrieve and parse snippet for table
+     * representation
+     */
+    getData : function (cb) {
+
+      var match = this._match;
+      try {
+        KorAP.API.getTextInfo(
+          match, {}, function (textResponse) {
+            
+            if (textResponse === undefined) {
+              cb(null);
+              return;
+            };
+
+            var doc = textResponse["document"];
+       
+            if (doc === undefined) {
+              cb(null);
+              return;
+            };
+
+            var fields = doc["fields"];
+            if (fields === undefined) {
+              cb(null);
+              return;
+            };
+
+            // Add metainfo to matchview
+            cb(matchMetaClass.create(
+              match, fields
+            ));
+          }
+        );
+      }
+      catch (e) {
+        KorAP.log(0, e);
+        cb(null);
+      };
+    },
+
+    // Delete circular references
+    onClose : function () {
+      this._match = undefined;
+    }
+  }
+});
diff --git a/dev/js/src/view/match/relations.js b/dev/js/src/view/match/relations.js
new file mode 100644
index 0000000..fda2cde
--- /dev/null
+++ b/dev/js/src/view/match/relations.js
@@ -0,0 +1,171 @@
+define([
+  'view',
+  'match/treehierarchy',
+  'match/treearc'
+], function (viewClass, matchTreeHierarchyClass, matchTreeArcClass) {
+
+  const d = document;
+  const loc   = KorAP.Locale;
+  loc.DOWNLOAD = loc.DOWNLOAD || 'Download';
+
+  
+  return {
+    create : function (match,foundry,layer,type) {
+      return Object.create(viewClass)._init(['relations']).upgradeTo(this)._init(match, foundry, layer, type);
+    },
+
+
+    // Initialize relations object
+    _init : function (match,foundry, layer, type) {
+      this._match = match;
+      this._foundry = foundry;
+      this._layer = layer;
+      this._type = type;
+      return this;
+    },
+    
+
+    /**
+     * Meta view element
+     */
+    show : function () {
+
+      if (this._show)
+        return this._show;
+
+      var matchtree = d.createElement('div');
+      matchtree.classList.add('matchtree', 'loading');
+
+      // this.element().appendChild(matchtree);
+
+      // Add title line
+      var h6 = matchtree.addE('h6');
+      h6.addE('span').addT(this._foundry);
+      h6.addE('span').addT(this._layer);      
+
+      this._tree = matchtree.addE('div');
+
+      this._show = matchtree;
+      return matchtree;
+    },
+
+
+    /**
+     * Do after embedding
+     */
+    afterEmbed : function () {
+
+      var foundry = this._foundry,
+          layer = this._layer,
+          type = this._type;
+
+      var that = this;
+      var tree = this._tree;
+      var matchtree = this._show;
+
+      // Get tree data async
+      this.getData(foundry, layer, type, function (treeObj) {
+
+        matchtree.classList.remove('loading');
+
+        // Something went wrong - probably log!!!
+
+        if (treeObj === null) {
+          tree.addT('No data available.');
+        }
+        else {
+          tree.appendChild(treeObj.element());
+          treeObj.show();
+
+          // Reposition the view to the center
+          // (This may in a future release be a reposition
+          // to move the root to the actual match)
+
+          // This is currently not supported by relations
+          if (type === "spans") {
+
+            // Download link
+            that.actions.add(loc.DOWNLOAD, ['button-icon','download'], function (e) {
+              var a = treeObj.downloadLink();
+              d.body.appendChild(a);
+              a.click();
+              d.body.removeChild(a)
+            });
+          };
+          
+          treeObj.center();
+        };
+  
+        /*
+          if (cb)
+          cb(treeObj);
+          */
+      });
+
+      matchtree.classList.remove('loading');
+    },
+
+    /**
+     * Get match object
+     */
+    match : function () {
+      return this._match;
+    },
+
+
+    /**
+     * Retrieve and parse snippet for relation
+     * representation
+     */
+    getData : function (foundry, layer, type, cb) {
+      var focus = [];
+
+      try {
+        // TODO: Support and cache multiple trees
+        KorAP.API.getMatchInfo(
+          this._match, {
+            'spans' : true,
+            'foundry' : foundry,
+            'layer' : layer
+          },
+          function (matchResponse) {
+            if (matchResponse === undefined) {
+              cb(null);
+              return;
+            };
+
+            // Get snippet from match info
+            if (matchResponse["snippet"] !== undefined) {
+
+              // Todo: This should be cached somehow
+
+              if (type === "spans") {
+                cb(matchTreeHierarchyClass.create(matchResponse["snippet"]));
+              }
+              else if (type === "rels") {
+                cb(matchTreeArcClass.create(matchResponse["snippet"]));              
+              }
+
+              // Unknown tree type
+              else {
+                cb(null);
+              };
+            }
+            else {
+              cb(null);
+            };
+          }.bind(this)
+        );
+      }
+      catch (e) {
+        KorAP.log(0, e);
+        cb(null);
+      };
+    },
+  
+    // Delete circular references
+    onClose : function () {
+      this._match = undefined;
+    }
+  }
+});
diff --git a/dev/js/src/view/match/tokentable.js b/dev/js/src/view/match/tokentable.js
new file mode 100644
index 0000000..70e8a8f
--- /dev/null
+++ b/dev/js/src/view/match/tokentable.js
@@ -0,0 +1,135 @@
+define([
+  'view',
+  'match/table',
+  'match/infolayer'
+], function (viewClass, matchTableClass, infoLayerClass) {
+
+  const d = document;
+  
+  return {
+    create : function (match) {
+      return Object.create(viewClass)._init(['tokentable']).upgradeTo(this)._init(match);
+    },
+
+
+    _init : function (match) {
+      console.log(match);
+      this._match = match;
+      return this;
+    },
+    
+    /**
+     * TokenTable view element
+     */
+    show : function () {
+      if (this._show)
+        return this._show;
+
+      // Append default table
+      var matchtable = d.createElement('div');
+      matchtable.classList.add('matchtable', 'loading');
+
+      var that = this;
+
+      // TODO:
+      //   Create try-catch-exception-handling
+      
+      // Create the table asynchronous
+      this.getData(undefined, function (table) {
+
+        // Load data
+        matchtable.classList.remove('loading');
+
+        if (table !== null) {
+          matchtable.appendChild(table.element());
+	      };
+      });
+
+      // TODO:
+      //   Loading should have a timeout on view-level
+      //   matchtable.classList.remove('loading');
+      
+      this._show = matchtable;
+      return matchtable;
+    },
+
+
+    /**
+     * Get match object
+     */
+    match : function () {
+      return this._match;
+    },
+
+
+    /**
+     * Retrieve and parse snippet for table
+     * representation
+     */
+    getData : function (tokens, cb) {
+      var focus = [];
+
+      // Get all tokens
+      if (tokens === undefined) {
+        focus = this._match.getTokens();
+      } 
+
+      // Get only some tokens
+      else {
+        
+        // Push newly to focus array
+        for (var i = 0; i < tokens.length; i++) {
+          var term = tokens[i];
+          try {
+            // Create info layer objects
+            var layer = infoLayerClass.create(term);
+            layer.type = "tokens";
+            focus.push(layer);
+          }
+          catch (e) {
+            continue;
+          };
+        };
+      };
+      
+      // No tokens chosen
+      if (focus.length == 0)
+        cb(null);
+
+      try {
+        // Get info (may be cached)
+        KorAP.API.getMatchInfo(
+          this._match,
+          { 'spans' : false, 'layer' : focus },
+        
+          // Callback for retrieval
+          function (matchResponse) {
+
+            if (matchResponse === undefined)
+              cb(null);
+
+            // Get snippet from match info
+            if (matchResponse["snippet"] !== undefined) {
+              this._table = matchTableClass.create(matchResponse["snippet"]);
+              cb(this._table);
+            };
+          }.bind(this)
+        );
+      }
+      catch (e) {
+        KorAP.log(0, e);
+        cb(null);
+      };
+
+      /*
+      // Todo: Store the table as a hash of the focus
+      return null;
+      */
+    },
+
+    // Delete circular references
+    onClose : function () {
+      this._match = undefined;
+    }
+  }
+});