Merge morphTable and morphTree to matchInfo module
diff --git a/public/js/runner/matchInfo.html b/public/js/runner/matchInfo.html
new file mode 100644
index 0000000..3d274fc
--- /dev/null
+++ b/public/js/runner/matchInfo.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Spec Runner for Morph Table View</title>
+  <link rel="shortcut icon" type="image/png" href="../lib/jasmine-2.1.1/jasmine_favicon.png">
+  <link rel="stylesheet" href="../lib/jasmine-2.1.1/jasmine.css">
+  <script src="../lib/jasmine-2.1.1/jasmine.js"></script>
+  <script src="../lib/jasmine-2.1.1/jasmine-html.js"></script>
+  <script src="../lib/jasmine-2.1.1/boot.js"></script>
+  <script src="../src/matchInfo.js"></script>
+  <script src="../spec/matchInfoSpec.js"></script>
+</head>
+<body>
+</body>
+</html>
diff --git a/public/js/spec/matchInfoSpec.js b/public/js/spec/matchInfoSpec.js
new file mode 100644
index 0000000..dc0d4a1
--- /dev/null
+++ b/public/js/spec/matchInfoSpec.js
@@ -0,0 +1,178 @@
+describe('KorAP.InfoLayer', function () {
+  it('should be initializable', function () {
+    expect(
+      function() { KorAP.InfoLayer.create() }
+    ).toThrow(new Error("Missing parameters"));
+
+    expect(
+      function() { KorAP.InfoLayer.create("base") }
+    ).toThrow(new Error("Missing parameters"));
+
+    var layer = KorAP.InfoLayer.create("base", "s");
+    expect(layer).toBeTruthy();
+    expect(layer.foundry).toEqual("base");
+    expect(layer.layer).toEqual("s");
+    expect(layer.type).toEqual("tokens");
+
+    layer = KorAP.InfoLayer.create("cnx", "syn", "spans");
+    expect(layer).toBeTruthy();
+    expect(layer.foundry).toEqual("cnx");
+    expect(layer.layer).toEqual("syn");
+    expect(layer.type).toEqual("spans");
+  });
+});
+
+describe('KorAP.Match', function () {
+  var match = {
+    'corpusID' : 'WPD',
+    'docID' : 'UUU',
+    'textID' : '01912',
+    'pos' : 'p121-122'
+  };
+
+  it('should be initializable', function () {
+    var mInfo = KorAP.Match.create(match);
+    expect(mInfo.corpusID).toEqual("WPD");
+  });
+});
+
+describe('KorAP.MatchInfo', function () {
+  var available = [
+    'base/s=spans',
+    'corenlp/c=spans',
+    'corenlp/ne=tokens',
+    'corenlp/p=tokens',
+    'corenlp/s=spans',
+    'glemm/l=tokens',
+    'mate/l=tokens',
+    'mate/m=tokens',
+    'mate/p=tokens',
+    'opennlp/p=tokens',
+    'opennlp/s=spans',
+    'tt/l=tokens',
+    'tt/p=tokens',
+    'tt/s=spans'
+  ];
+
+  var match = {
+    'corpusID' : 'WPD',
+    'docID' : 'UUU',
+    'textID' : '01912',
+    'pos' : 'p121-122'
+  };
+
+  var snippet = "<span title=\"cnx/l:meist\">" +
+    "  <span title=\"cnx/p:ADV\">" +
+    "    <span title=\"cnx/syn:@PREMOD\">" +
+    "      <span title=\"mate/l:meist\">" +
+    "        <span title=\"mate/p:ADV\">" +
+    "          <span title=\"opennlp/p:ADV\">meist</span>" +
+    "        </span>" +
+    "      </span>" +
+    "    </span>" +
+    "  </span>" +
+    "</span>" +
+    "<span title=\"cnx/l:deutlich\">" +
+    "  <span title=\"cnx/p:A\">" +
+    "    <span title=\"cnx/syn:@PREMOD\">" +
+    "      <span title=\"mate/l:deutlich\">" +
+    "        <span title=\"mate/m:degree:pos\">" +
+    "          <span title=\"mate/p:ADJD\">" +
+    "            <span title=\"opennlp/p:ADJD\">deutlich</span>" +
+    "          </span>" +
+    "        </span>" +
+    "      </span>" +
+    "    </span>" +
+    "  </span>" +
+    "</span>" +
+    "<span title=\"cnx/l:fähig\">" +
+    "  <span title=\"cnx/l:leistung\">" +
+    "    <span title=\"cnx/p:A\">" +
+    "      <span title=\"cnx/syn:@NH\">" +
+    "        <span title=\"mate/l:leistungsfähig\">" +
+    "          <span title=\"mate/m:degree:comp\">" +
+    "            <span title=\"mate/p:ADJD\">" +
+    "              <span title=\"opennlp/p:ADJD\">leistungsfähiger</span>" +
+    "            </span>" +
+    "          </span>" +
+    "        </span>" +
+    "      </span>" +
+    "    </span>" +
+    "  </span>" +
+    "</span>";
+
+
+  it('should be initializable', function () {
+    expect(function() {
+      KorAP.MatchInfo.create()
+    }).toThrow(new Error('Missing parameters'));
+
+    expect(function() {
+      KorAP.MatchInfo.create(available)
+    }).toThrow(new Error('Missing parameters'));
+
+    expect(KorAP.MatchInfo.create(match, available)).toBeTruthy();
+
+    // /corpus/WPD/UUU.01912/p121-122/matchInfo?spans=false&foundry=*
+    var info = KorAP.MatchInfo.create(match, available);
+
+    // Spans:
+    var spans = info.getSpans();
+    expect(spans[0].foundry).toEqual("base");
+    expect(spans[0].layer).toEqual("s");
+
+    expect(spans[1].foundry).toEqual("corenlp");
+    expect(spans[1].layer).toEqual("c");
+
+    expect(spans[2].foundry).toEqual("corenlp");
+    expect(spans[2].layer).toEqual("s");
+
+    expect(spans[spans.length-1].foundry).toEqual("tt");
+    expect(spans[spans.length-1].layer).toEqual("s");
+
+    // Tokens:
+    var tokens = info.getTokens();
+    expect(tokens[0].foundry).toEqual("corenlp");
+    expect(tokens[0].layer).toEqual("ne");
+
+    expect(tokens[1].foundry).toEqual("corenlp");
+    expect(tokens[1].layer).toEqual("p");
+
+    expect(tokens[tokens.length-1].foundry).toEqual("tt");
+    expect(tokens[tokens.length-1].layer).toEqual("p");
+  });
+
+
+  it('should parse into a table', function () {
+    var info = KorAP.MatchInfo.create(match, available);
+
+    expect(info.getTable('base/s')).not.toBeTruthy();
+
+    // Override getMatchInfo API call
+    KorAP.API.getMatchInfo = function() {
+      return { "snippet": snippet };
+    };
+
+    var table = info.getTable();
+    expect(table).toBeTruthy();
+
+    expect(table.length()).toBe(3);
+
+    expect(table.getToken(0)).toBe("meist");
+    expect(table.getToken(1)).toBe("deutlich");
+    expect(table.getToken(2)).toBe("leistungsfähiger");
+
+    expect(table.getValue(0, "cnx", "p")[0]).toBe("ADV");
+    expect(table.getValue(0, "cnx", "syn")[0]).toBe("@PREMOD");
+
+    expect(table.getValue(2, "cnx", "l")[0]).toBe("fähig");
+    expect(table.getValue(2, "cnx", "l")[1]).toBe("leistung");
+  });
+
+});
+
+// table = view.toTable();
+// table.sortBy('');
+// table.element();
+// tree = view.toTree();
+// tree.element();
diff --git a/public/js/spec/menuSpec.js b/public/js/spec/menuSpec.js
index a818574..08fea76 100644
--- a/public/js/spec/menuSpec.js
+++ b/public/js/spec/menuSpec.js
@@ -328,6 +328,18 @@
     ['Autor', 'author']
   ];
 
+  var demolonglist = [
+    ['Titel', 'title'],
+    ['Untertitel', 'subTitle'],
+    ['Veröffentlichungsdatum', 'pubDate'],
+    ['Länge', 'length'],
+    ['Autor', 'author'],
+    ['Genre', 'genre'],
+    ['corpusID', 'corpusID'],
+    ['docID', 'docID'],
+    ['textID', 'textID'],
+  ];
+
   it('should be initializable', function () {
     var list = [
       ["Constituency"],
@@ -689,7 +701,7 @@
 
     // Change show
     expect(menu.prefix("e").show()).toBe(true);
-
+    expect(menu._prefix.active()).toBe(false);
     expect(menu.shownItem(0).name()).toEqual("Constituency");
     expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Constitu<mark>e</mark>ncy</strong><span><mark>E</mark>xampl<mark>e</mark> 1</span>");
     expect(menu.shownItem(0).active()).toBe(true);
@@ -935,6 +947,7 @@
     expect(menu.shownItem(2)).toBe(undefined);
   });
 
+
   it('should be navigatable with a prefix (2)', function () {
     var menu = KorAP.HintMenu.create("cnx/", demolist);
     menu.limit(3);
@@ -943,7 +956,6 @@
     menu.prefix('el');
     expect(menu.show()).toBe(true);
 
-
     expect(menu.prefix()).toEqual("el");
     expect(menu._prefix.active()).toEqual(false);
     expect(menu.shownItem(0).name()).toEqual("Titel");
@@ -979,6 +991,52 @@
     expect(menu.shownItem(2)).toBe(undefined);
   });
 
-  xit('should be page downable');
+  it('should be navigatable with a prefix (3)', function () {
+    var menu = KorAP.HintMenu.create("cnx/", demolist);
+    menu.limit(3);
+    expect(menu.show()).toBe(true);
+    expect(menu.prefix()).toEqual("");
+    menu.prefix('el');
+    expect(menu.show()).toBe(true);
+
+    expect(menu.prefix()).toEqual("el");
+    expect(menu._prefix.active()).toEqual(false);
+    expect(menu.shownItem(0).name()).toEqual("Titel");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+    expect(menu.shownItem(0).active()).toBe(true);
+    expect(menu.shownItem(1).name()).toEqual("Untertitel");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+    expect(menu.shownItem(1).active()).toBe(false);
+    expect(menu.shownItem(2)).toBe(undefined);
+
+    // Backward
+    menu.prev();
+    expect(menu._prefix.active()).toEqual(true);
+    expect(menu.shownItem(0).name()).toEqual("Titel");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+    expect(menu.shownItem(0).active()).toBe(false);
+    expect(menu.shownItem(1).name()).toEqual("Untertitel");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+    expect(menu.shownItem(1).active()).toBe(false);
+    expect(menu.shownItem(2)).toBe(undefined);
+
+
+    // Forward
+    menu.next();
+    expect(menu.prefix()).toEqual("el");
+    expect(menu._prefix.active()).toEqual(false);
+    expect(menu.shownItem(0).name()).toEqual("Titel");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+    expect(menu.shownItem(0).active()).toBe(true);
+    expect(menu.shownItem(1).name()).toEqual("Untertitel");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+    expect(menu.shownItem(1).active()).toBe(false);
+    expect(menu.shownItem(2)).toBe(undefined);
+
+  });
+
+  it('should be page downable', function () {
+    
+  });
   xit('should be page upable');
 });
diff --git a/public/js/src/matchInfo.js b/public/js/src/matchInfo.js
new file mode 100644
index 0000000..515a78f
--- /dev/null
+++ b/public/js/src/matchInfo.js
@@ -0,0 +1,319 @@
+/**
+ * Make annotations visible.
+ *
+ * @author Nils Diewald
+ */
+/*
+  - Scroll with a static left legend.
+  - Highlight (at least mark as bold) the match
+  - Scroll to match vertically per default
+ */
+var KorAP = KorAP || {};
+
+(function (KorAP) {
+  "use strict";
+
+  KorAP._AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
+  KorAP._TermRE = new RegExp("^([^\/]+?)(?:\/([^:]+?))?:(.+?)$");
+  KorAP._matchTerms  = ["corpusID", "docID", "textID"];
+
+  // API requests
+  KorAP.API = KorAP.API || {};
+  KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () { return {} };
+
+  KorAP.MatchInfo = {
+
+    /**
+     * Create a new annotation object.
+     * Expects an array of available foundry/layer=type terms.
+     * Supported types are 'spans', 'tokens' and 'rels'.
+     */
+    create : function (match, available) {
+      if (arguments.length < 2)
+	throw new Error("Missing parameters");
+
+      return Object.create(KorAP.MatchInfo)._init(match, available);
+    },
+
+    _init : function (match, available) {
+      this._match = KorAP.Match.create(match);
+      this._available = {
+	tokens : [],
+	spans : [],
+	rels : []
+      };
+      for (var i = 0; i < available.length; i++) {
+	var term = available[i];
+	// Create info layer objects
+	try {
+	  var layer = KorAP.InfoLayer.create(term);
+	  this._available[layer.type].push(layer);
+	}
+	catch (e) {
+	  continue;
+	};
+      };
+      return this;
+    },
+
+
+    /**
+     * Return a list of parseable tree annotations.
+     */
+    getSpans : function () {
+      return this._available.spans;
+    },
+
+
+    /**
+     * Return a list of parseable token annotations.
+     */
+    getTokens : function () {
+      return this._available.tokens;
+    },
+
+
+    /**
+     * Return a list of parseable relation annotations.
+     */
+    getRels : function () {
+      return this._available.rels;
+    },
+
+
+    getTable : function (tokens) {
+      var focus = [];
+
+      // Get all tokens
+      if (tokens === undefined) {
+	focus = this.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 = KorAP.InfoLayer.create(term);
+	    layer.type = "tokens";
+	    focus.push(layer);
+	  }
+	  catch (e) {
+	    continue;
+	  };
+	};
+      };
+
+      // No tokens chosen
+      if (focus.length == 0)
+	return;
+
+      // Get info (may be cached)
+      var matchResponse = KorAP.API.getMatchInfo(
+	this._match,
+	{ 'spans' : true, 'layer' : focus }
+      );
+
+      // Get snippet from match info
+      if (matchResponse["snippet"] !== undefined) {
+	this._table = KorAP.InfoTable.create(matchResponse["snippet"]);
+	return this._table;
+      };
+
+      return null;
+    }
+
+    /*
+    // Parse snippet for table visualization
+    getTree : function (foundry, layer) {
+    },
+    */
+  };
+
+  KorAP.Match = {
+    create : function (match) {
+      return Object.create(KorAP.Match)._init(match);
+    },
+    _init : function (match) {
+      for (var i in KorAP._matchTerms) {
+	var term = KorAP._matchTerms[i];
+	if (match[term] !== undefined) {
+	  this[term] = match[term];
+	}
+	else {
+	  this[term] = undefined;
+	}
+      };
+      return this;
+    },
+  };
+
+  /**
+   *
+   * Alternatively pass a string as <tt>base/s=span</tt>
+   *
+   * @param foundry
+   */
+  KorAP.InfoLayer = {
+    create : function (foundry, layer, type) {
+      return Object.create(KorAP.InfoLayer)._init(foundry, layer, type);
+    },
+    _init : function (foundry, layer, type) {
+      if (foundry === undefined)
+	throw new Error("Missing parameters");
+
+      if (layer === undefined) {
+	if (KorAP._AvailableRE.exec(foundry)) {
+	  this.foundry = RegExp.$1;
+	  this.layer = RegExp.$2;
+	  this.type = RegExp.$3;
+	}
+	else {
+	  throw new Error("Missing parameters");
+	};
+      }
+      else {
+	this.foundry = foundry;
+	this.layer = layer;
+	this.type = type;
+      };
+
+      if (this.type === undefined)
+	this.type = 'tokens';
+
+      return this;
+    }
+  };
+
+
+  KorAP.InfoTable = {
+    create : function (snippet) {
+      return Object.create(KorAP.InfoTable)._init(snippet);
+    },
+    _init : function (snippet) {
+      // Create html for traversal
+      var html = document.createElement("div");
+      html.innerHTML = snippet;
+
+      this._pos = 0;
+      this._token = [];
+      this._info = [];
+      this._foundry = [];
+      this._layer = [];
+
+      // Parse the snippet
+      this._parse(html.childNodes);      
+
+      this._layer = undefined;
+      this._foundry = undefined;
+
+      html.innerHTML = '';
+      return this;
+    },
+
+    length : function () {
+      return this._pos;
+    },
+
+    getToken : function (pos) {
+      if (pos === undefined)
+	return this._token;
+      return this._token[pos];
+    },
+
+    getValue : function (pos, foundry, layer) {
+      return this._info[pos][foundry + '/' + layer]
+    },
+
+    getLayerPerFoundry : function (foundry) {
+      return this._foundry[foundry]
+    },
+
+    getFoundryPerLayer : function (layer) {
+      return this._layer[layer];
+    },
+
+    // Parse the snippet
+    _parse : function (children) {
+
+      // Get all children
+      for (var i in children) {
+	var c = children[i];
+
+	// Create object on position unless it exists
+	if (this._info[this._pos] === undefined)
+	  this._info[this._pos] = {};
+
+	// Store at position in foundry/layer as array
+	var found = this._info[this._pos];
+
+	// Element with title
+	if (c.nodeType === 1) {
+	  if (c.getAttribute("title") &&
+	      KorAP._TermRE.exec(c.getAttribute("title"))) {
+
+	    // Fill position with info
+	    var foundry, layer, value;
+	    if (RegExp.$2) {
+	      foundry = RegExp.$1;
+	      layer   = RegExp.$2;
+	    }
+	    else {
+	      foundry = "base";
+	      layer   = RegExp.$1
+	    };
+
+	    value = RegExp.$3;
+
+	    if (found[foundry + "/" + layer] === undefined)
+	      found[foundry + "/" + layer] = [];
+
+	    // Push value to foundry/layer at correct position
+	    found[foundry + "/" + layer].push(RegExp.$3);
+
+	    // Set foundry
+	    if (!this._foundry[foundry])
+	      this._foundry[foundry] = {};
+	    this._foundry[foundry][layer] = 1;
+
+	    // Set layer
+	    if (!this._layer[layer])
+	      this._layer[layer] = {};
+	    this._layer[layer][foundry] = 1;
+	  };
+
+	  // depth search
+	  if (c.hasChildNodes())
+	    this._parse(c.childNodes);
+	}
+
+	// Leaf node - store string on position and go to next string
+	else if (c.nodeType === 3) {
+	  if (c.nodeValue.match(/[a-z0-9]/i))
+	    this._token[this._pos++] = c.nodeValue;
+	};
+      };
+
+      delete this._info[this._pos];
+    },
+    element : function () {
+      var ce = document.createElement;
+      // First the legend table
+      /*
+      var table = ce('table');
+      var row = ce('tr');
+      table.appendChild(tr);
+      */      
+    }
+  };
+
+  
+  /*
+    KorAP.InfoFoundryLayer = {};
+    KorAP.InfoTree = {};
+    KorAP.InfoTable = {};
+  */
+}(this.KorAP));
diff --git a/public/js/src/menu.js b/public/js/src/menu.js
index 3d0017d..8ccf740 100644
--- a/public/js/src/menu.js
+++ b/public/js/src/menu.js
@@ -83,6 +83,9 @@
 	this.next();
 	break;
       case 39: // 'Right'
+	if (this._prefix.active())
+	  break;
+
 	var item = this.liveItem(this._position);
 	if (item["further"] !== undefined) {
 	  item["further"].bind(item).apply();
@@ -223,6 +226,7 @@
      * @param {string} Prefix for filtering the list
      */
     show : function () {
+
       // Initialize the list
       if (!this._initList())
 	return false;
@@ -233,6 +237,7 @@
       // Set the first element to active
       // Todo: Or the last element chosen
       this.liveItem(0).active(true);
+      this._prefix.active(false);
 
       this._active = this._list[0];
       this._position = 0;
@@ -466,8 +471,13 @@
       var newItem;
 
       // Set new live item
-      var oldItem = this.liveItem(this._position++);
-      oldItem.active(false);
+      if (!this._prefix.active()) {
+	var oldItem = this.liveItem(this._position);
+	oldItem.active(false);
+      };
+
+      this._position++;
+
       newItem = this.liveItem(this._position);
 
       // The next element is undefined - roll to top or to prefix
@@ -501,6 +511,42 @@
       newItem.active(true);
     },
 
+    /*
+     * Page down to the first item on the next page
+     */
+    /*
+    nextPage : function () {
+
+      // Prefix is active
+      if (this._prefix.active()) {
+	this._prefix.active(false);
+      }
+
+      // Last item is chosen
+      else if (this._position >= this.limit() + this._offset) {
+
+	this._position = this.limit() + this._offset - 1;
+	newItem = this.liveItem(this._position);
+	var oldItem = this.liveItem(this._position--);
+	oldItem.active(false);
+      }
+
+      // Last item of page is chosen
+      else if (0) {
+
+      // Jump to last item
+      else {
+	var oldItem = this.liveItem(this._position);
+	oldItem.active(false);
+
+	this._position = this.limit() + this._offset - 1;
+	newItem = this.liveItem(this._position);
+      };
+
+      newItem.active(true);
+    },
+	*/
+
 
     /*
      * Make the previous item in the menu active
@@ -508,8 +554,10 @@
     prev : function () {
 
       // No active element set
-      if (this._position == -1)
+      if (this._position === -1) {
 	return;
+	// TODO: Choose last item
+      };
 
       var newItem;
 
@@ -534,7 +582,6 @@
 	this._position = this.liveLength() - 1;
 
 	if (prefix.isSet() && !prefix.active()) {
-
 	  this._position++;
 	  prefix.active(true);
 	  return;
@@ -792,6 +839,10 @@
       // Add prefix span
       this._element = document.createElement('span');
       this._element.classList.add('pref');
+      // Connect action
+      if (this.onclick !== undefined)
+	this._element["onclick"] = this.onclick.bind(this);
+
       return this;
     },
     _update : function () {