ES6: port menu

Change-Id: I8740d81b928e26215276f3ce92c87d83dbc3a7b2
diff --git a/.gitignore b/.gitignore
index 5c2a411..4ede8f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,6 @@
 /sandbox
 /Sandbox
 node_modules
-/local/
 /cache/data
 /public
 /kalamar.*.conf
diff --git a/Changes b/Changes
index 6bbc0b6..1d00ec9 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,4 @@
+        - Migration of menu to ES6 (diewald)
         - Migration of util and datepicker to ES6 (hebasta)
 
 0.66 2026-07-15
diff --git a/dev/js/src-lib/menu/demo/index.html b/dev/js/src-lib/menu/demo/index.html
new file mode 100644
index 0000000..5407b55
--- /dev/null
+++ b/dev/js/src-lib/menu/demo/index.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Menu demo</title>
+    <meta charset="utf-8" />
+  </head>
+  <body>
+    <div id="menu" style="padding-top: 80px; padding-left: 80px;"></div>
+
+    <script type="module">
+      import '../../../../scss/kalamar.scss';
+      import KalamarMenu from '../menu.js';
+
+      const menu = new KalamarMenu([
+        ["Constituency", "c=", "Example 1"],
+        ["Lemma", "l="],
+        ["Morphology", "m=", "Example 2"],
+        ["Part-of-Speech", "p="],
+        ["Syntax", "syn="]
+      ]);
+
+      document.getElementById('menu').appendChild(menu.element());
+      menu.limit(3).show(3);
+      menu.focus();
+    </script>
+  </body>
+</html>
diff --git a/dev/js/src-lib/menu/menu.js b/dev/js/src-lib/menu/menu.js
new file mode 100644
index 0000000..d9b2e0f
--- /dev/null
+++ b/dev/js/src-lib/menu/menu.js
@@ -0,0 +1,964 @@
+/**
+ * Scrollable drop-down menus with view filter.
+ *
+ * @author Nils Diewald
+ */
+/*
+ * TODO: Show the slider briefly on move (whenever screen is called).
+ * TODO: Ignore alt+ and strg+ key strokes.
+ * TODO: Should scroll to a chosen value after prefixing, if the chosen value is live
+ * TODO: Add a "title" to a menu that is not scrollable.
+ * TODO: Make the menu responsive by showing less items on smaller screens
+ *       or anytime items would be outside the screen.
+ * TODO: Add a .match() method to items for scrolling and probably for prefixing.
+ * TODO: Add static header (for title, sortation fields, but also for menu points like "fragments" and "history".
+ * TODO: Support space separated list of prefixes so "co no" will highlight "common noun"
+ */
+
+import defaultItemClass from './menu/item';
+import defaultPrefixClass from './menu/prefix';
+import defaultLengthFieldClass from './menu/lengthField';
+import sliderClass from './menu/slider';
+
+const menuLimit = 8;
+
+export default class KalamarMenu {
+
+  /**
+   * List of items for drop down menu (complete).
+   * Only a sublist of the menu is filtered (live).
+   * Only a sublist of the filtered menu is visible (shown).
+   */
+
+    /**
+     * Create new Menu based on the action prefix
+     * and a list of menu items.
+     *
+     *
+     * Accepts an associative array containg the elements
+     * itemClass, prefixClass, lengthFieldClass
+     *
+     * @this {Menu}
+     * @constructor
+     * @param {string} Context prefix
+     * @param {Array.<Array.<string>>} List of menu items
+     */
+  constructor (list, params) {
+      return this._init(list, params);
+    }
+
+    // Initialize list
+    _init (list, params) {
+      if (params === undefined)
+        params = {};
+
+      const t = this;
+      t._notItemElements=3;
+
+      t._itemClass = params["itemClass"] || defaultItemClass;
+
+      // Add prefix object
+      if (params["prefixClass"] !== undefined) {
+        t._prefix = new params["prefixClass"]();
+      }
+      else {
+        t._prefix = new defaultPrefixClass();
+      };
+      t._prefix._menu = t;
+
+      // Add lengthField object
+      if (params["lengthFieldClass"] !== undefined) {
+        t._lengthField = new params["lengthFieldClass"]();
+      }
+      else {
+        t._lengthField = new defaultLengthFieldClass();
+      };
+      t._lengthField._menu = t;
+
+      // Initialize slider
+      t._slider = new sliderClass(t);
+
+      // Create the element
+      var el = document.createElement("ul");
+      el.style.outline = 0;
+      el.setAttribute('tabindex', 0);
+      el.classList.add('menu', 'roll');
+      el.appendChild(t._prefix.element());
+      el.appendChild(t._lengthField.element());
+      el.appendChild(t._slider.element());
+
+      // This has to be cleaned up later on
+      el["menu"] = t;
+
+      // Arrow keys
+      el.addEventListener(
+        'keydown',
+        t._keydown.bind(t),
+        false
+      );
+
+      // Strings
+      el.addEventListener(
+        'keypress',
+        t._keypress.bind(t),
+        false
+      );
+
+      // Mousewheel
+      el.addEventListener(
+        'wheel',
+        t._mousewheel.bind(t),
+        false
+      );
+
+      // Touch events
+      ['touchstart', 'touchend', 'touchmove'].forEach(
+        e => el.addEventListener(e, t._touch.bind(t), false)
+      );
+
+      
+      t._el = el;
+
+      t._limit = menuLimit;
+      
+      t._items = new Array(); //all childNodes, i.e. ItemClass, prefixClass
+
+      t.readItems(list);
+
+      t.dontHide = false;
+        
+      return t;
+    }
+
+    // Read items to add to list
+    readItems (list) {
+      const t = this;
+
+      t._list = undefined; //filtered List containing all itemClass items
+
+      // Remove circular reference to "this" in items
+      for (let i = 0; i < t._items.length; i++) {
+        delete t._items[i]["_menu"];
+        delete t._items[i];
+      };
+
+      t._items = new Array();
+      t.removeItems();
+
+
+      // Initialize items
+      t._lengthField.reset();
+
+      // Initialize item list based on parameters
+      list.forEach(function(i){
+        const obj = new this._itemClass(i);
+
+        // This may become circular
+        obj["_menu"] = this;
+        this._lengthField.add(i);
+        this._items.push(obj);
+      }, t);
+
+      t._slider.length(t.liveLength())
+        .limit(t._limit)
+        .reInit();
+      
+      t._firstActive = false;
+      // Show the first item active always?
+      t.offset = 0;
+      t.position = 0;
+    }
+
+    // Append item to list
+    append (item) {
+      const t = this;
+      // This is cyclic!
+      item["_menu"] = t;
+      t._list = undefined;
+      t.removeItems();
+      t._items.push(item);
+      t._lengthField.add([item.content().data]);
+      t._slider.length(t.liveLength()).reInit();
+      t._firstActive = false;
+      t.offset = 0;
+      t.position = 0;
+    }
+    
+    // Initialize the item list
+    // returns true if the length of the resulting list is at least 1
+    // and there was a prefix value. Returns true if there was no prefix value set.
+    _initList () {
+      // Upon change also update alwaysmenu.js please
+      const t = this;
+
+      // Create a new list
+      if (t._list === undefined) {
+        t._list = [];
+      }
+      else if (t._list.length !== 0) {
+        t._boundary(false);
+        t._list.length = 0;
+      };
+
+      // Offset is initially zero
+      t.offset = 0;
+
+      // There is no prefix set
+      if (t.prefix().length <= 0) {
+
+        // add all items to the list and lowlight
+        let i = 0;
+        for (; i < t._items.length; i++) {
+          t._list.push(i);
+          t._items[i].lowlight();
+        };
+
+        t._slider.length(i).reInit();
+
+        return true;
+      };
+
+      /*
+       * There is a prefix set, so filter the list!
+       */
+      let pos;
+      const prefixList = t.prefix().toLowerCase().split(" ");
+
+      const items = [];
+      let maxPoints = 1; // minimum 1
+
+      // Iterate over all items and choose preferred matching items
+      // i.e. the matching happens at the word start
+      t._items.forEach(function(it, pos){
+
+        let points = 0;
+
+        prefixList.forEach(function(p) {
+
+          // Check if it matches at the beginning
+          if ((it.lcField().includes(" " + p))) {
+            points += 5;
+          }
+
+          // Check if it matches anywhere
+          else if (it.lcField().includes(p)) {
+            points += 1;
+          };
+        });
+
+        if (points > maxPoints) {
+          this._list = [pos];
+          maxPoints = points;
+        }
+        else if (points == maxPoints) {
+          this._list.push(pos);
+        }
+      }, t);
+
+      t._slider.length(t._list.length).reInit();
+
+      // Filter was successful - yeah!
+      return t._list.length > 0 ? true : false;
+    }
+
+
+    /**
+     * Destroy this menu
+     * (in case you don't trust the
+     * mark and sweep GC)!
+     */
+    destroy () {
+      // Upon change also update alwaysmenu.js please
+      const t = this;
+
+      // Remove circular reference to "this" in menu
+      if (t._el != undefined)
+        delete t._el["menu"]; 
+
+      // Remove circular reference to "this" in items
+      t._items.forEach(function(i) {
+        delete i["_menu"];
+      });
+
+      // Remove circular reference to "this" in prefix
+      delete t._prefix['_menu'];
+      delete t._lengthField['_menu'];
+      delete t._slider['_menu'];
+    }
+
+
+    /**
+     * Focus on this menu.
+     */
+    focus () {
+      this._el.focus();
+    }
+
+
+    // mouse wheel treatment
+    _mousewheel (e) {
+      const delta = e.deltaY / 120;
+      if (delta > 0)
+        this.next();
+      else if (delta < 0)
+        this.prev();
+      e.halt();
+    }
+
+
+    // touchmove treatment
+    _touch (e) {
+      const s = this.slider();
+
+      if (e.type === "touchstart") {
+        this._lastTouch = e.touches[0].clientY;
+      }
+      else if (e.type === "touchend") {
+        this._lastTouch = undefined;
+      }
+      else if (e.type === "touchmove") {
+        const to = e.touches[0];
+
+        // TODO:
+        // Instead of using 26px, choose the item height
+        // or use the menu height // shownItems
+        
+        // s.movetoRel(t.clientY - this._initTouch);
+        if ((this._lastTouch + 26) < to.clientY) {
+          this.viewDown();
+          this._lastTouch = to.clientY;
+        }
+        else if ((this._lastTouch - 26) > to.clientY) {
+          this.viewUp();
+          this._lastTouch = to.clientY;
+        }
+        e.halt();
+      };
+    }
+
+    // Arrow key and prefix treatment
+    _keydown (e) {
+      //Upon change also update alwaysmenu.js please
+      const t = this;
+
+      switch (_codeFromEvent(e)) {
+
+      case 27: // 'Esc'
+        e.halt();
+        t.hide();
+        break;
+
+      case 38: // 'Up'
+        e.halt();
+        t.prev();
+        break;
+
+      case 33: // 'Page up'
+        e.halt();
+        t.pageUp();
+        break;
+
+      case 40: // 'Down'
+        e.halt();
+        t.next();
+        break;
+
+      case 34: // 'Page down'
+        e.halt();
+        t.pageDown();
+        break;
+
+      case 39: // 'Right'
+        if (t._prefix.active())
+          break;
+
+        const item = t.liveItem(t.position);
+        
+        if (item["further"] !== undefined) {
+          item["further"].bind(item).apply();
+        };
+        
+        e.halt();
+        break;
+
+      case 13: // 'Enter'
+        // Click on prefix
+        if (t._prefix.active())
+          t._prefix.onclick(e);
+
+        // Click on item
+        else
+          t.liveItem(t.position).onclick(e);
+        e.halt();
+        break;
+
+      case 8: // 'Backspace'
+        t._prefix.chop();
+        t.show();
+        e.halt();
+        break;
+      };
+    }
+
+
+    // Add characters to prefix
+    _keypress (e) {
+      if (e.charCode !== 0) {
+        e.halt();
+        
+        // Add prefix
+        this._prefix.add(
+          String.fromCharCode(_codeFromEvent(e))
+        );
+
+        this.show();
+      };
+    }
+
+
+    /**
+     * Show a screen with a given offset
+     * in the viewport.
+     */
+    screen (nr) {
+      const t = this;
+
+      // Normalize negative values
+      if (nr < 0) {
+        nr = 0
+      }
+
+      // The shown list already shows everything
+      else if (t.liveLength() < t.limit()) {
+        return false;
+      }
+
+      // Move relatively to the next screen
+      else if (nr > (t.liveLength() - t.limit())) {
+        nr = (t.liveLength() - t.limit());
+      };
+
+      // no change
+      if (t.offset === nr)
+        return false;
+
+      t._showItems(nr);
+
+      return true;
+    }
+
+
+    /**
+     * Get the associated dom element.
+     */
+    element () {
+      return this._el;
+    }
+
+
+    /**
+     * Get the creator class for items
+     */
+    itemClass () {
+      return this._itemClass;
+    }
+
+
+    /**
+     * Get and set the numerical value
+     * for the maximum number of items visible.
+     */
+    limit (limit) {
+      if (arguments.length === 1) {
+        if (this._limit !== limit) {
+          this._limit = limit;
+          this._slider.limit(limit).reInit();
+        };
+        return this;
+      };
+      return this._limit;
+    }
+
+
+    /**
+     * Filter the list and make it visible.
+     * This is always called once the prefix changes.
+     *
+     * @param {string} Prefix for filtering the list
+     */
+    show (active) {
+      //Upon change please also update alwaysmenu.js and containermenu.js (only two lines new there)
+      const t = this;
+
+      // show menu based on initial offset
+      t._unmark();     // Unmark everything that was marked before
+      t.removeItems();
+
+      // Initialize the list
+      if (!t._initList()) {
+
+        // The prefix is not active
+        t._prefix.active(true);
+
+        // finally show the element
+        t._el.classList.add('visible');
+        
+        return true;
+      };
+
+      let offset = 0;
+
+      // Set a chosen value to active and move the viewport
+      if (arguments.length === 1) {
+
+        // Normalize active value
+        if (active < 0) {
+          active = 0;
+        }
+        else if (active >= t.liveLength()) {
+          active = t.liveLength() - 1;
+        };
+
+        // Item is outside the first viewport
+        if (active >= t._limit) {
+          offset = active;
+          const newOffset = t.liveLength() - t._limit;
+          if (offset > newOffset) {
+            offset = newOffset;
+          };
+        };
+        
+        t.position = active;
+      }
+
+      // Choose the first item
+      else if (t._firstActive) {
+        t.position = 0;
+      }
+
+      // Choose no item
+      else {
+        t.position = -1;
+      };
+
+      t.offset = offset;
+      t._showItems(offset); // Show new item list
+
+      // Make chosen value active
+      if (t.position !== -1) {
+        t.liveItem(t.position).active(true);
+      };
+
+      // The prefix is not active
+      t._prefix.active(false);
+
+      // finally show the element
+      t._el.classList.add('visible');
+
+      // Add classes for rolling menus
+      t._boundary(true);
+
+      return true;
+    }
+
+
+    /**
+     * Hide the menu and call the onHide callback.
+     */
+    hide () {
+      if (!this.dontHide) {
+        this.removeItems();
+        this._prefix.clear();
+        this.onHide();
+        this._el.classList.remove('visible');
+      }
+      // this._el.blur();
+    }
+
+
+    /**
+     * Function released when the menu hides.
+     * This method is expected to be overridden.
+     */
+    onHide () {}
+
+
+    /**
+     * Get the prefix for filtering,
+     * e.g. &quot;ve&quot; for &quot;verb&quot;
+     */
+    prefix (pref) {
+      if (arguments.length === 1) {
+        this._prefix.value(pref);
+        return this;
+      };
+      return this._prefix.value();
+    }
+
+    
+    /**
+     * Get the lengthField object.
+     */
+    lengthField () {
+      return this._lengthField;
+    }
+
+
+    /**
+     * Get the associated slider object.
+     */
+    slider () {
+      return this._slider;
+    }
+
+
+    /**
+     * Delete all visible items from the menu element
+     */
+    
+    removeItems () {
+      const liElements=this._el.getElementsByTagName("LI");
+      var ignoredCount = 0; //counts how many LI tag elements are not actually direct children
+      while (liElements.length>ignoredCount){
+        if (liElements[ignoredCount].parentNode === this._el){
+          this._el.removeChild(liElements[ignoredCount]);
+        } else {
+          ignoredCount++;
+        }
+      };
+     }
+      
+
+
+    /**
+     * Get a specific item from the complete list
+     *
+     * @param {number} index of the list item
+     */
+    item (index) {
+      return this._items[index]
+    }
+
+
+    /**
+     * Get a specific item from the filtered list
+     *
+     * @param {number} index of the list item
+     *        in the filtered list
+     */
+    liveItem (index) {
+      if (this._list === undefined)
+        if (!this._initList())
+          return;
+
+      return this._items[this._list[index]];
+    }
+
+
+    /**
+     * Get a specific item from the viewport list
+     *
+     * @param {number} index of the list item
+     *        in the visible list
+     */
+    shownItem (index) {
+      if (index >= this.limit())
+        return;
+
+      return this.liveItem(this.offset + index);
+    }
+
+
+    /**
+     * Get the length of the full item list
+     */
+    length () {
+      return this._items.length;
+    }
+
+
+    /**
+     * Length of the filtered item list.
+     */
+    liveLength () {
+      if (this._list === undefined)
+        this._initList();
+      return this._list.length;
+    }
+
+    
+    /**
+     * Make the next item in the filtered menu active
+     */
+    next () {
+      //Upon change please update alwaysmenu.js next
+      const t = this;
+
+      // No list
+      if (t.liveLength() === 0)
+        return;
+
+      // Deactivate old item
+      if (t.position !== -1 && !t._prefix.active()) {
+        t.liveItem(t.position).active(false);
+      };
+
+      // Get new active item
+      t.position++;
+      let newItem = t.liveItem(t.position);
+
+      // The next element is undefined - roll to top or to prefix
+      if (newItem === undefined) {
+
+        // Activate prefix
+        const prefix = this._prefix;
+
+        // Prefix is set and not active - choose!
+        if (prefix.isSet() && !prefix.active()) {
+          t.position--;
+          prefix.active(true);
+          return;
+        }
+
+        // Choose first item
+        else {
+          newItem = t.liveItem(0);
+          // choose first item
+          t.position = 0;
+          t._showItems(0);
+        };
+      }
+
+      // The next element is after the viewport - roll down
+      else if (t.position >= (t.limit() + t.offset)) {
+        t.screen(t.position - t.limit() + 1);
+      }
+
+      // The next element is before the viewport - roll up
+      else if (t.position <= t.offset) {
+        t.screen(t.position);
+      };
+
+      t._prefix.active(false);
+      newItem.active(true);
+    }
+
+
+    /*
+     * Make the previous item in the menu active
+     */
+    prev () {
+      //Upon Change please update alwaysmenu.js prev
+      const t = this;
+
+      // No list
+      if (t.liveLength() === 0)
+        return;
+
+      // Deactivate old item
+      if (!t._prefix.active()) {
+
+        // No active element set
+        if (t.position === -1) {
+          t.position = t.liveLength();
+        }
+
+        // No active element set
+        else {
+          t.liveItem(t.position--).active(false);
+        };
+      };
+
+      // Get new active item
+      let newItem = t.liveItem(t.position);
+
+      // The previous element is undefined - roll to bottom
+      if (newItem === undefined) {
+
+        // Activate prefix
+        const prefix = t._prefix;
+        let offset =  t.liveLength() - t.limit();
+        
+        // Normalize offset
+        offset = offset < 0 ? 0 : offset;
+
+        // Choose the last item
+        t.position = t.liveLength() - 1;
+        
+        // Prefix is set and not active - choose!
+        if (prefix.isSet() && !prefix.active()) {
+          t.position++;
+          prefix.active(true);
+          t.offset = offset;
+          return;
+        }
+
+        // Choose last item
+        else {
+          newItem = t.liveItem(t.position);
+          t._showItems(offset);
+        };
+      }
+
+      // The previous element is before the view - roll up
+      else if (t.position < t.offset) {
+        t.screen(t.position);
+      }
+
+      // The previous element is after the view - roll down
+      else if (t.position >= (t.limit() + t.offset)) {
+        t.screen(t.position - t.limit() + 2);
+      };
+
+      t._prefix.active(false);
+      newItem.active(true);
+    }
+
+
+    /**
+     * Move the page up by limit!
+     */
+    pageUp () {
+      this.screen(this.offset - this.limit());
+    }
+
+
+    /**
+     * Move the page down by limit!
+     */
+    pageDown () {
+      this.screen(this.offset + this.limit());
+    }
+
+
+    /**
+     * Move the view one item up
+     */
+    viewUp () {
+      this.screen(this.offset - 1);
+    }
+
+
+    /**
+     * Move the view one item down
+     */
+    viewDown () {
+      this.screen(this.offset + 1);
+    }
+
+    /**
+     * Reset the prefix. Currently not used in regular menu.
+     */
+    reset () {
+      this.prefix("");
+    }
+
+    // Unmark all items
+    _unmark () {
+      this._list.forEach(function(it){
+        const item = this._items[it];
+        item.lowlight();
+        item.active(false);
+      }, this);
+    }
+
+
+    // Set boundary for viewport
+    _boundary (bool) {
+      if (this._list.length === 0)
+        return;
+
+      this.item(this._list[0]).noMore(bool);
+      this.item(this._list[this._list.length - 1]).noMore(bool);
+    }
+
+
+    // Append Items that should be shown
+    _showItems (off) {
+      const t = this;
+
+      // optimization: scroll down one step
+      if (t.offset === (off - 1)) {
+        t.offset = off;
+
+        // Remove the HTML node from the first item
+        // leave lengthField/prefix/slider
+        //console.log("_showItems, at _notItemElements is: ",t._el.children[this._notItemElements]);
+        t._el.removeChild(t._el.children[this._notItemElements]);
+
+        t._append(
+          t._list[t.offset + t.limit() - 1]
+        );
+      }
+
+      // optimization: scroll up one step
+      else if (t.offset === (off + 1)) {
+        t.offset = off;
+
+        // Remove the HTML node from the last item
+        //console.log("_showItems, at lastChild is: ",t._el.lastChild);
+        t._el.removeChild(t._el.lastChild);
+
+        t._prepend(t._list[t.offset]);
+      }
+
+      else {
+        t.offset = off;
+
+        // Remove all items
+        t.removeItems();
+
+        // Use list
+        let shown = 0;
+
+        for (let i = 0; i < t._list.length; i++) {
+
+          // Don't show - it's before offset
+          shown++;
+          if (shown <= off)
+            continue;
+
+          t._append(t._list[i]);
+          
+          if (shown >= (t.limit() + off))
+            break;
+        };
+      };
+
+      // set the slider to the new offset
+      t._slider.offset(t.offset);
+    }
+
+
+    // Append item to the shown list based on index
+    _append (i) {
+      const item = this.item(i);
+
+      // Highlight based on prefix
+      if (this.prefix().length > 0) {
+        item.highlight(this.prefix().toLowerCase());
+      };
+
+      // Append element
+      this.element().appendChild(item.element());
+    }
+
+
+    // Prepend item to the shown list based on index
+    _prepend (i) {
+      const item = this.item(i);
+
+      // Highlight based on prefix
+      if (this.prefix().length > 0) {
+        item.highlight(this.prefix().toLowerCase());
+      };
+
+      const e = this.element();
+
+      // Append element after lengthField/prefix/slider
+      e.insertBefore(
+        item.element(),
+        e.children[this._notItemElements]
+      );
+    }
+};
diff --git a/dev/js/src-lib/menu/menu/item.js b/dev/js/src-lib/menu/menu/item.js
new file mode 100644
index 0000000..7ad4366
--- /dev/null
+++ b/dev/js/src-lib/menu/menu/item.js
@@ -0,0 +1,292 @@
+/*
+ * MenuItems may define:
+ *
+ * onclick: action happen on click and enter.
+ * further: action happen on right arrow
+ */
+
+/**
+ * Item in the Dropdown menu
+ */
+export default class KalamarMenuItem {
+  /**
+   * Create a new MenuItem object.
+   *
+   * @constructor
+   * @this {MenuItem}
+   * @param {Array.<string>} An array object of name, action and
+   *   optionally a description
+   */
+  constructor (params) {
+    return this._init(params);
+  }
+
+
+  /**
+   * Get or set the content of the meun item.
+   */
+  content (content) {
+    if (arguments.length === 1)
+      this._content = document.createTextNode(content);
+    return this._content;
+  }
+
+
+  /**
+   * Get or set the information for action of this item. 
+   */
+  action (action) {
+    if (arguments.length === 1)
+      this._action = action;
+    return this._action;
+  }
+  
+
+  /**
+   * Get the lower cased field of the item
+   * (used for analyses).
+   */
+  lcField () {
+    return this._lcField;
+  }
+
+
+  /**
+   * Check or set if the item is active
+   *
+   * @param {boolean|null} State of activity
+   */
+  active (bool) {
+    const cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("active");
+    else if (bool)
+      cl.add("active");
+    else
+      cl.remove("active");
+  }
+
+
+  /**
+   * Check or set if the item is
+   * at the boundary of the menu
+   * list
+   *
+   * @param {boolean|null} State of activity
+   */
+  noMore (bool) {
+    const cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("no-more");
+    else if (bool)
+      cl.add("no-more");
+    else
+      cl.remove("no-more");
+  }
+  
+  /**
+   * Get the document element of the menu item
+   */
+  element () {
+    // already defined
+    if (this._el !== undefined)
+      return this._el;
+    
+    // Create list item
+    const li = document.createElement("li");
+
+    // Connect action
+    if (this["onclick"] !== undefined) {
+      li["onclick"] = this.onclick.bind(this);
+    };
+
+    // Append template
+    li.appendChild(this.content());
+    
+    return this._el = li;
+  }
+
+  /**
+   * Highlight parts of the item
+   *
+   * @param {string} Prefix string for highlights
+   */
+  highlight (prefix) {
+
+    // The prefix already matches
+    if (this._prefix === prefix)
+      return;
+
+    // There is a prefix but it doesn't match
+    if (this._prefix !== null) {
+      this.lowlight();
+    }
+
+    const children = this.element().childNodes;
+    for (let i = children.length -1; i >= 0; i--) {
+      this._highlight(children[i], prefix);
+    };
+
+    this._prefix = prefix;
+  }
+
+
+  /**
+   * Remove highlight of the menu item
+   */
+  lowlight () {
+    if (this._prefix === null)
+      return;
+
+    const e = this.element();
+    
+    const marks = e.getElementsByTagName("mark");
+
+    for (let i = marks.length - 1; i >= 0; i--) {
+
+      // Replace with content
+      marks[i].parentNode.replaceChild(
+        // Create text node clone
+	      document.createTextNode(
+	        marks[i].firstChild.nodeValue
+        ),
+	      marks[i]
+      );
+    };
+
+    // Remove consecutive textnodes
+    e.normalize();
+    this._prefix = null;
+  }
+
+
+  // Highlight a certain substring of the menu item
+  _highlight (elem, prefixString) {    
+    if (elem.nodeType === 3) {
+      
+      const text   = elem.nodeValue;
+      const textlc = text.toLowerCase();
+
+      // Split prefixes
+      if (prefixString) {
+
+        // ND:
+        //   Doing this in a single line can trigger
+        //   a deep-recursion in Firefox 57.01, though I don't know why.
+        prefixString = prefixString.trim();
+
+        const prefixes = prefixString.split(" ");
+
+        let testPos,
+            pos = -1,
+            len = 0;
+
+        // Iterate over all prefixes and get the best one
+        // for (var i = 0; i < prefixes.length; i++) {
+        prefixes.forEach(function(i) {
+
+          // Get first pos of a matching prefix
+          testPos = textlc.indexOf(i);
+          if (testPos < 0)
+            return;
+
+          if (pos === -1 || testPos < pos) {
+            pos = testPos;
+            len = i.length;
+          }
+          else if (testPos === pos && i.length > len) {
+            len = i.length;
+          };
+        });
+
+        // Matches!
+        if (pos >= 0) {
+	
+	        // First element
+	        if (pos > 0) {
+	          elem.parentNode.insertBefore(
+	            document.createTextNode(text.substr(0, pos)),
+	            elem
+	          );
+	        };
+	
+	        // Second element
+	        const hl = document.createElement("mark");
+	        hl.appendChild(
+	          document.createTextNode(text.substr(pos, len))
+	        );
+	        elem.parentNode.insertBefore(hl, elem);
+	
+	        // Third element
+	        const third = text.substr(pos + len);
+
+	        if (third.length > 0) {
+	          const thirdE = document.createTextNode(third);
+	          elem.parentNode.insertBefore(
+	            thirdE,
+	            elem
+	          );
+	          this._highlight(thirdE, prefixString);
+	        };
+	
+          elem.parentNode.removeChild(elem);
+        };
+      };
+    }
+
+    else {
+      const children = elem.childNodes;
+      for (let i = children.length -1; i >= 0; i--) {
+	      this._highlight(children[i], prefixString);
+      };
+    };
+  }
+
+  // Initialize menu item
+  _init (params) {
+    
+    if (params[0] === undefined) {
+      throw new Error("Missing parameters");
+    };
+
+    const t = this;
+
+    t.content(params[0]);
+    
+    if (params.length > 1) {
+      t._action = params[1];
+
+      if (params.length > 2)
+        t._onclick = params[2];
+    };
+    
+    t._lcField = ' ' + t.content().textContent.toLowerCase();
+    t._prefix = null;
+
+    return this;
+  }
+
+
+  /**
+   * The click action of the menu item.
+   */
+  onclick (e) {
+    const m = this.menu();
+
+    // Reset prefix
+    m.prefix("");
+
+    if (this._onclick)
+      this._onclick.apply(this, e);
+
+    m.hide();
+  }
+
+  
+  /**
+   * Return menu list.
+   */
+  menu () {
+    return this._menu;
+  }
+};
diff --git a/dev/js/src-lib/menu/menu/lengthField.js b/dev/js/src-lib/menu/menu/lengthField.js
new file mode 100644
index 0000000..3612b22
--- /dev/null
+++ b/dev/js/src-lib/menu/menu/lengthField.js
@@ -0,0 +1,44 @@
+export default class KalamarMenuLengthField {
+
+  /**
+   * Create new lengthField object.
+   */
+  constructor () {
+    return this._init();
+  }
+
+
+  // Initialize lengthField object
+  _init () {
+    this._el = document.createElement('div');
+    this._el.classList.add('lengthField');
+    return this;
+  }
+
+
+  /**
+   * Get the associated dom element.
+   */
+  element () {
+    return this._el;
+  }
+
+
+  /**
+   * Add string to lengthField.
+   */
+  add (param) {
+    this._el.appendChild(document.createElement('span'))
+      .appendChild(document.createTextNode(param[0] + '--'));
+  }
+
+
+  /**
+   * Remove all initialized values
+   */
+  reset () {
+    while (this._el.firstChild) {
+      this._el.firstChild.remove();
+    };
+  }
+};
diff --git a/dev/js/src-lib/menu/menu/prefix.js b/dev/js/src-lib/menu/menu/prefix.js
new file mode 100644
index 0000000..2c15d03
--- /dev/null
+++ b/dev/js/src-lib/menu/menu/prefix.js
@@ -0,0 +1,130 @@
+export default class KalamarMenuPrefix {
+
+  /**
+   * Create new prefix object.
+   */
+  constructor () {
+    return this._init();
+  }
+
+
+  // Initialize prefix object
+  _init () {
+    const t = this;
+
+    t._string = '';
+
+    // Add prefix span
+    t._el = document.createElement('span');
+    t._el.classList.add('non-item');
+    t._el.classList.add('pref');
+    // Connect action
+
+    if (t["onclick"] !== undefined) {
+      t._el["onclick"] = t.onclick.bind(t);
+    }
+    return t;
+  }
+
+
+  _update () {
+    return this._el.innerHTML
+      = this._string;
+  }
+
+
+  /**
+   * Get or set the activity status of the prefix.
+   */
+  active (bool) {
+    const cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("active");
+    else if (bool)
+      cl.add("active");
+    else
+      cl.remove("active");
+  }
+
+
+  /**
+   * Check, if a prefix is given or not.
+   */
+  isSet () {
+    return this._string.length > 0 ?
+      true : false;
+  }
+
+
+  /**
+   * Get or set the prefix string.
+   */
+  value (string) {
+    if (arguments.length === 1) {
+      this._string = string;
+      return this._update();
+    };
+    return this._string;
+  }
+
+
+  /**
+   * Add string to prefix.
+   */
+  add (string) {
+    this._string += string;
+    return this._update();
+  }
+  
+
+  /**
+   * Clear prefix
+   */
+  clear () {
+    this._string = '';
+    return this._update();
+  }
+
+
+  /**
+   * Action method.
+   * Expected to be overridden.
+   */
+  onclick () {}
+
+  
+  /**
+   * Remove the last character of the string
+   */
+  chop () {
+    const t = this;
+
+    // Prefix is long enough for backspace
+    if (t._string.length > 1) {
+      t._string = t._string.substring(
+	      0, t._string.length - 1
+      );
+    }
+    else {
+      t._string = '';
+    };
+    
+    return t._update();
+  }
+
+
+  /**
+   * Get the associated dom element.
+   */
+  element () {
+    return this._el;
+  }
+
+
+  /**
+   * Return menu list.
+   */
+  menu () {
+    return this._menu;
+  }
+};
diff --git a/dev/js/src-lib/menu/menu/slider.js b/dev/js/src-lib/menu/menu/slider.js
new file mode 100644
index 0000000..70a96c9
--- /dev/null
+++ b/dev/js/src-lib/menu/menu/slider.js
@@ -0,0 +1,260 @@
+/**
+ * Create slider for menus.
+ * The slider will only be used by mouse - touch support
+ * shouldn't be necessary, as the menu can be scrolled using touch.
+ *
+ * @author Nils Diewald
+ */
+export default class KalamarMenuSlider {
+
+  /**
+   * Create new slider for Menu.
+   * @this {Slider}
+   * @constructor
+   * @param {Menu} menu object
+   */
+  constructor (menu) {
+    return this._init(menu);
+  }
+
+
+  /**
+   * Length attribute of the slider
+   * (as number of items).
+   *
+   * @param {number} Number of items (optional)
+   */
+  length (i) {
+    if (arguments.length === 0)
+      return this._length;
+    if (i == this._length)
+      return this;
+    this._length = i;
+    return this;
+  }
+
+
+  /**
+   * Limit of items per screen.
+   *
+   * @param {number} Number of items per screen (optional)
+   */
+  limit (i) {
+    if (arguments.length === 0)
+      return this._limit;
+    if (i == this._limit)
+      return this;
+    this._limit = i;
+    return this;
+  }
+
+
+  /**
+   * Is the slider active or not.
+   *
+   * @param {bool} true or false (optional)
+   */
+  active (bool) {
+    if (arguments.length === 1) {
+      if (bool) {
+	      if (!this._active) {
+	        this._el.classList.add('active');
+	        this._active = true;
+	      };
+      }
+      else if (this._active) {
+	      this._el.classList.remove('active');
+	      this._active = false;
+      }
+    };
+    return this._active;
+  }
+
+
+  /**
+   * Move the slider to a relative position
+   *
+   * @param {number} relative position
+   */
+  movetoRel (relativePos) {
+
+    // This is important to find the correct percentage!
+    const diffHeight = (this._rulerHeight - this._sliderHeight);
+    const relativeOffset = (relativePos / diffHeight);
+
+    // Offset is a value 0 to this._screens
+    const off = this.offset(
+      parseInt(relativeOffset * this._screens) + this._event.initOffset
+    );
+
+    if (off !== undefined) {
+      this._menu.screen(off);
+    };
+  }
+
+
+  /**
+   * Move the slider to an absolute position
+   *
+   * @param {number} absolute position
+   */
+  movetoAbs (absPos) {
+    const absOffset = (absPos / this._rulerHeight);
+    const off = this.offset(parseInt(absOffset * (this._screens + 1)));
+
+    if (off !== undefined) {
+      this._menu.screen(off);
+    };
+  }
+
+
+  /**
+   * Screen offset of the slider
+   *
+   * @param {number} Offset position of the slider (optional)
+   */
+  offset (off) {
+    if (arguments.length === 0)
+      return this._offset;
+
+    // Normalize offset
+    if (off > this._screens)
+      off = this._screens;
+    else if (off < 0)
+      off = 0;
+
+    // Identical with old value
+    if (off === this._offset)
+      return undefined;
+
+    // Set offset and move
+    this._offset = off;
+    this._slider.style.top = (this._step * off) + '%';
+    return off;
+  }
+
+
+  /**
+   * Get the associated dom element.
+   */
+  element () {
+    return this._el;
+  }
+
+
+  /**
+   * Reinitialize the size of the slider.
+   * Necessary to call after each adjustment of the list.
+   */
+  reInit () {
+
+    const t = this;
+    const s = t._el.style;
+
+    // Do not show the slider, in case there is nothing to scroll
+    if (t._length <= t._limit) {
+      s.display = 'none';
+      return;
+    }
+    else {
+      s.display = 'block';
+    };
+
+    t._height  = ((t._limit / t._length) * 100);
+    t._screens = t._length - t._limit;
+    t._step    = (100 - t._height) / t._screens;
+    t._slider.style.height = t._height + '%';
+  }
+
+
+  // Initialize prefix object
+  _init (menu) {
+    const t = this;
+
+    t._menu = menu;
+
+    t._offset = 0;
+    t._event = {};
+    t._active = false;
+
+    const el = t._el = document.createElement('div');
+    el.setAttribute('class', 'ruler');
+
+    t._slider = el.appendChild(
+      document.createElement('span')
+    );
+
+    t._ruler = el.appendChild(document.createElement('div'));
+
+    // Do not mark the menu on mousedown
+    t._ruler.addEventListener('mousedown', function (e) {
+      e.halt()
+    }, false);
+
+    // Move the slider to the click position
+    t._ruler.addEventListener('click', t._mouseclick.bind(t), false);
+
+    t._slider.addEventListener('mousedown', t._mousedown.bind(t), false);
+
+    return t;
+  }
+
+
+  // Reinit height based on dom position
+  _initClientHeight () {
+    this._rulerHeight  = this._el.clientHeight;
+    this._sliderHeight = this._slider.clientHeight;
+  }
+
+
+  // Release mousemove event
+  _mousemove (e) {
+    this.movetoRel(e.clientY - this._event.init);
+    e.halt();
+  }
+
+
+  // Release mouseup event
+  _mouseup (e) {
+    this.active(false);
+    window.removeEventListener('mousemove', this._event.mov);
+    window.removeEventListener('mouseup', this._event.up);
+    this._menu.focus();
+  }
+
+
+  // Release mousedown event
+  _mousedown (e) {
+    // Bind drag handler
+    const ev = this._event;
+
+    // _step * _offset is the distance of the ruler to the top
+    ev.init = e.clientY;
+    ev.initOffset = this._offset;
+    // By substracting that, it is like initializing to the first point
+
+    ev.mov  = this._mousemove.bind(this);
+    ev.up   = this._mouseup.bind(this);
+
+    // TODO: This may not be necessary all the time
+    this._initClientHeight();
+
+    this.active(true);
+
+    window.addEventListener('mousemove', ev.mov);
+    window.addEventListener('mouseup', ev.up);
+
+    e.halt();
+  }
+
+
+  // Release event to reposition slider on ruler
+  _mouseclick (e) {
+    this._initClientHeight();
+
+    this.movetoAbs(
+      e.clientY - this._ruler.getClientRects()[0].top
+    );
+    e.halt();
+  }
+};
diff --git a/dev/js/src-lib/menu/package.json b/dev/js/src-lib/menu/package.json
new file mode 100644
index 0000000..75fcb36
--- /dev/null
+++ b/dev/js/src-lib/menu/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "kalamar-menu",
+  "version": "0.0.1",
+  "description": "Scrollable drop-down menu with view filters",
+  "main": "menu.js",
+  "scripts": {
+    "test": "vitest",
+    "dev": "vite --open /demo/index.html"
+  },
+  "author": "",
+  "license": "ISC"
+}
diff --git a/dev/js/src-lib/menu/test/menuSpec.js b/dev/js/src-lib/menu/test/menuSpec.js
new file mode 100644
index 0000000..1fe5c56
--- /dev/null
+++ b/dev/js/src-lib/menu/test/menuSpec.js
@@ -0,0 +1,1933 @@
+import { describe, it, expect } from 'vitest';
+import menuClass from '../menu.js';
+import menuItemClass from '../menu/item.js';
+import prefixClass from '../menu/prefix.js';
+import lengthFieldClass from '../menu/lengthField.js';
+
+function directElementChildrenByTagName(element, tagName) {
+  const result = [];
+  const children = element.childNodes;
+  const upperTag = tagName.toUpperCase();
+  for (let i = 0; i < children.length; i++) {
+    if (children[i].nodeName === upperTag && children[i].parentNode === element) {
+      result.push(children[i]);
+    }
+  }
+  return result;
+}
+
+    // The OwnMenu item
+    class OwnMenuItem extends menuItemClass {
+      content (content) {
+        if (arguments.length === 1) {
+	        this._content = content;
+        };
+        return this._content;
+      }
+      _init (params) {
+        if (params[0] === undefined)
+	        throw new Error("Missing parameters");
+
+        this._content = document.createTextNode(params[0]);
+        this._lcField = ' ' + this.content().textContent.toLowerCase();
+
+        return this;
+      }
+    };
+
+    // The OwnMenu
+    class OwnMenu extends menuClass {
+      constructor (list) {
+        super(list, { itemClass : OwnMenuItem });
+      }
+    };
+
+
+    // HintMenuItem
+    class HintMenuItem extends menuItemClass {
+      content (content) {
+        if (arguments.length === 1) {
+	        this._content = content;
+        };
+        return this._content;
+      }
+      _init (params) {
+        if (params[0] === undefined || params[1] === undefined)
+	        throw new Error("Missing parameters");
+
+        this._name   = params[0];
+        this._action = params[1];
+        this._lcField = ' ' + this._name.toLowerCase();
+
+        if (params.length > 2) {
+	        this._desc = params[2];
+	        this._lcField += " " + this._desc.toLowerCase();
+        };
+        
+        return this;
+      }
+
+      name () {
+        return this._name;
+      }
+      action () {
+        return this._action;
+      }
+      desc () {
+        return this._desc;
+      }
+      element () {
+        // already defined
+        if (this._el !== undefined)
+	        return this._el;
+
+        // Create list item
+        var li = document.createElement("li");
+        li.setAttribute("data-action", this._action);
+
+        // Create title
+        var name =  document.createElement("strong");
+        name.appendChild(document.createTextNode(this._name));
+        
+        li.appendChild(name);
+
+        // Create description
+        if (this._desc !== undefined) {
+	        var desc = document.createElement("span");
+	        desc.appendChild(document.createTextNode(this._desc));
+	        li.appendChild(desc);
+        };
+        return this._el = li;
+      }
+    };
+
+
+    // HintMenu
+    class HintMenu extends menuClass {
+      constructor (context, list) {
+        super(list, {itemClass : HintMenuItem});
+        this._context = context;
+      }
+    };
+
+
+    // The ComplexMenuItem
+    class ComplexMenuItem extends menuItemClass {
+      content (content) {
+        if (arguments.length === 1) {
+	        this._content = content;
+        };
+        return this._content;
+      }
+      _init (params) {
+        if (params[0] === undefined)
+	        throw new Error("Missing parameters");
+
+        var r = document.createElement('div');
+        for (var i = 1; i <= params.length; i++) {
+	        var h = document.createElement('h' + i);
+	        h.appendChild(document.createTextNode(params[i-1]));
+	        r.appendChild(h);
+        };
+
+        this._content = r;
+        this._lcField = ' ' + this.content().textContent.toLowerCase();
+
+        return this;
+      }
+    };
+
+
+    describe('KorAP.MenuItem', function () {
+      it('should be initializable', function () {
+        expect(
+	        function() { new menuItemClass([]) }
+        ).toThrow(new Error("Missing parameters"));
+
+        expect(
+	        function() { new OwnMenuItem([]) }
+        ).toThrow(new Error("Missing parameters"));
+
+        var mi = new OwnMenuItem(["Baum"]);
+        expect(mi.element().firstChild.nodeValue).toEqual('Baum');
+        expect(mi.lcField()).toEqual(' baum');
+      });
+
+      it('shouldn\'t have a reference to the menu', function () {
+        var menuItem = new OwnMenuItem(['Test']);
+        expect(menuItem.menu()).toBe(undefined);
+      });
+
+      it('should be activatable and deactivateable by class', function () {
+        var menuItem = new OwnMenuItem(['Test']);
+
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toBe(null);
+        menuItem.active(true);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.element().getAttribute("class")).toEqual("active");
+        menuItem.active(false); // Is active
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toEqual("");
+        menuItem.active(true);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.element().getAttribute("class")).toEqual("active");
+
+        menuItem = new OwnMenuItem(['Spiegel']);
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toBe(null);
+        menuItem.active(false); // Is not active
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toBe(null);
+      });
+
+      it('should be set to boundary', function () {
+        var menuItem = new OwnMenuItem(['CoreNLP']);
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toBe(null);
+
+        // Set active
+        menuItem.active(true);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.noMore()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toEqual("active");
+
+        // Set no more
+        menuItem.noMore(true);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.noMore()).toBe(true);
+        expect(menuItem.element().getAttribute("class")).toEqual("active no-more");
+
+        // No no more
+        menuItem.noMore(false);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.noMore()).toBe(false);
+        expect(menuItem.element().getAttribute("class")).toEqual("active");
+
+        // Set no more, deactivate
+        menuItem.noMore(true);
+        menuItem.active(false);
+        expect(menuItem.active()).toBe(false);
+        expect(menuItem.noMore()).toBe(true);
+        expect(menuItem.element().getAttribute("class")).toEqual("no-more");
+
+        // Set active
+        menuItem.active(true);
+        expect(menuItem.active()).toBe(true);
+        expect(menuItem.noMore()).toBe(true);
+        expect(menuItem.element().getAttribute("class")).toEqual("no-more active");
+      });
+
+
+      it('should be highlightable', function () {
+        // Highlight in the middle
+        var menuItem = new OwnMenuItem(['CoreNLP']);
+        menuItem.highlight("ren");
+        expect(menuItem.element().innerHTML).toEqual("Co<mark>reN</mark>LP");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual("CoreNLP");
+
+        var plain = "<div><h1>CoreNLP</h1><h2>corenlp/</h2></div>";
+
+        // Starting highlight
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/']);
+        menuItem.highlight("cor");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1><mark>Cor</mark>eNLP</h1><h2><mark>cor</mark>enlp/</h2></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Starting highlight - short
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/']);
+        menuItem.highlight("c");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1><mark>C</mark>oreNLP</h1><h2><mark>c</mark>orenlp/</h2></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight at the end
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/']);
+        menuItem.highlight("nlp");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>Core<mark>NLP</mark></h1><h2>core<mark>nlp</mark>/</h2></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight at the end - short
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/']);
+        menuItem.highlight("p");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>CoreNL<mark>P</mark></h1><h2>corenl<mark>p</mark>/</h2></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // No highlight
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/']);
+        menuItem.highlight("xp");
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight in the middle - first
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/', 'This is my Example']);
+        menuItem.highlight("ren");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>Co<mark>reN</mark>LP</h1><h2>co<mark>ren</mark>lp/</h2><h3>This is my Example</h3></div>");
+
+        plain = "<div><h1>CoreNLP</h1><h2>corenlp/</h2><h3>This is my Example</h3></div>"
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight in the middle - second
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/', 'This is my Example']);
+        menuItem.highlight("ampl");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>CoreNLP</h1><h2>corenlp/</h2><h3>This is my Ex<mark>ampl</mark>e</h3></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight in the middle - both
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/', 'This is my Example']);
+        menuItem.highlight("e");
+
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>Cor<mark>e</mark>NLP</h1><h2>cor<mark>e</mark>nlp/</h2><h3>This is my <mark>E</mark>xampl<mark>e</mark></h3></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight in the end - second
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/', 'This is my Example']);
+        menuItem.highlight("le");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>CoreNLP</h1><h2>corenlp/</h2><h3>This is my Examp<mark>le</mark></h3></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+
+        // Highlight at the beginning - second
+        menuItem = new ComplexMenuItem(['CoreNLP', 'corenlp/', 'This is my Example']);
+        menuItem.highlight("this");
+        expect(menuItem.element().innerHTML).toEqual("<div><h1>CoreNLP</h1><h2>corenlp/</h2><h3><mark>This</mark> is my Example</h3></div>");
+
+        menuItem.lowlight();
+        expect(menuItem.element().innerHTML).toEqual(plain);
+      });
+    });
+
+
+    describe('KorAP.Menu', function () {
+      var list = [
+        ["Constituency", "c=", "Example 1"],
+        ["Lemma", "l="],
+        ["Morphology", "m=", "Example 2"],
+        ["Part-of-Speech", "p="],
+        ["Syntax", "syn="]
+      ];
+
+      var listMultiPrefix = [
+        ["PP","PP ","Personal Pronoun"],
+        ["PPP","PPP ","Personal Pronoun, Plural"],
+        ["PPPA","PPPA ","Personal Pronoun, Plural, Acc."],
+        ["PPPD","PPPD ","Personal Pronoun, Plural, Dative"],
+        ["PPPR","PPPR ","Personal Pronoun, Plural, Direct"],
+        ["PPPO","PPPO ","Personal Pronoun, Plural, Oblique"],
+        ["PPS","PPS ","Personal Pronoun, Singular"],
+        ["PPSA","PPSA ","Personal Pronoun, Singular, Accusative"],
+        ["PPSD","PPSD ","Personal Pronoun, Singular, Dative"],
+        ["PPSR","PPSR ","Personal Pronoun, Singular, Direct"],
+        ["PPSN","PPSN ","Personal Pronoun, Singular, Nominative"],
+        ["PPSO","PPSO ","Personal Pronoun, Singular, Oblique"]
+      ];
+      
+      var demolist = [
+        ['Titel', 'title'],
+        ['Untertitel', 'subTitle'],
+        ['Veröffentlichungsdatum', 'pubDate'],
+        ['Länge', 'length'],
+        ['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"],
+	        ["Lemma"],
+	        ["Morphology"],
+	        ["Part-of-Speech"],
+	        ["Syntax"]
+        ];
+
+        var menu = new OwnMenu(list);
+        expect(menu.dontHide).toBe(false);
+        menu._firstActive = true;
+        expect(menu.itemClass()).toEqual(OwnMenuItem);
+        expect(menu.element().nodeName).toEqual('UL');
+        expect(menu.element().classList.contains('visible')).toBeFalsy();
+        expect(menu.limit()).toEqual(8);
+        expect(menu._notItemElements).toEqual(3);
+
+        menu.limit(9);
+        expect(menu.limit()).toEqual(9);
+
+        menu.limit(8);
+
+        // view
+        menu.show();
+
+        // First element in list
+        expect(menu.item(0).active()).toBe(true);
+        expect(menu.item(0).noMore()).toBe(true);
+
+        // Middle element in list
+        expect(menu.item(2).active()).toBe(false);
+        expect(menu.item(2).noMore()).toBe(false);
+
+        // Last element in list
+        expect(menu.item(menu.length() - 1).active()).toBe(false);
+        expect(menu.item(menu.length() - 1).noMore()).toBe(true);
+      });
+
+      it('should have a reference to the menu', function () {
+        var menu = new HintMenu("cnx/", list);
+        expect(menu.item(0).menu()).toEqual(menu);
+
+        menu = new HintMenu("cnx/", list);
+        expect(menu.element().menu).toEqual(menu);
+      });
+      
+      it('should only remove direct descendants with removeItems', function () {
+        var menu = new HintMenu("cnx/", list);
+        var newUL = document.createElement("ul");
+        var newLI = document.createElement("li");
+        newUL.appendChild(newLI);
+        //This is a very constructed example, but this actually happens within containerMenu
+        menu.element().appendChild(newUL);
+        expect(menu.element().childNodes[3].nodeName).toEqual("UL");
+        expect(menu.element().childNodes[3].childNodes[0]).toEqual(newLI);
+        menu.prefix("a"); //to call show
+        expect(menu.element().childNodes[3].nodeName).toEqual("UL");
+        expect(menu.element().childNodes[3].childNodes[0]).toEqual(newLI);
+        
+      });
+
+
+      it('should be visible', function () {
+        var menu = new HintMenu("cnx/", list);
+        expect(menu.removeItems()).toBe(undefined);
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].getAttribute("data-action")).toEqual("l=");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Check boundaries
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].classList.contains("no-more")).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].classList.contains("no-more")).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].classList.contains("no-more")).toBe(false);
+      });
+
+      it('should be filterable', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(3);
+        expect(menu.prefix("o").show()).toBe(true);
+        expect(menu.element().getElementsByClassName("pref")[0].innerHTML).toEqual("o");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Part-<mark>o</mark>f-Speech</strong>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Check boundaries
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].classList.contains("no-more")).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].classList.contains("no-more")).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].classList.contains("no-more")).toBe(true);
+
+        menu.limit(2);
+
+        expect(menu.prefix("o").show()).toBe(true);
+        expect(menu.element().getElementsByClassName("pref")[0].innerHTML).toEqual("o");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2]).toBe(undefined);
+
+        // Check boundaries
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].classList.contains("no-more")).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].classList.contains("no-more")).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2]).toBe(undefined);
+
+        expect(menu.prefix("e").show()).toBe(true);
+        expect(menu.element().getElementsByClassName("pref")[0].innerHTML).toEqual("e");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constitu<mark>e</mark>ncy</strong><span><mark>E</mark>xampl<mark>e</mark> 1</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2]).toBe(undefined);
+
+        menu.limit(5);
+        expect(menu.prefix("a").show()).toBe(true);
+        expect(menu.element().getElementsByClassName("pref")[0].innerHTML).toEqual("a");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Ex<mark>a</mark>mple 1</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemm<mark>a</mark></strong>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Ex<mark>a</mark>mple 2</span>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[3].innerHTML).toEqual("<strong>P<mark>a</mark>rt-of-Speech</strong>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[4].innerHTML).toEqual("<strong>Synt<mark>a</mark>x</strong>");
+        expect(directElementChildrenByTagName(menu.element(),"li")[5]).toBe(undefined);
+      });
+
+      it('should be nextable', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu._firstActive = true;
+
+        // Show only 3 items
+        menu.limit(3);
+        expect(menu.show()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (1)
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (2)
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (3)
+        // scroll!
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (4)
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Syntax</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (5) - ROLL
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Active next (6)
+        menu.next();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+      });
+
+      it('should be nextable without active field', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(3);
+        expect(menu.show()).toBe(true);
+        menu.next();
+        expect(menu.shownItem(0).active()).toEqual(true);
+      });
+
+
+      it('should be prevable', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu._firstActive = true;
+        menu.limit(3);
+        expect(menu.show()).toBe(true);
+
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Lemma</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (1) - roll to bottom
+        menu.prev();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Syntax</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (2)
+        menu.prev();
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Syntax</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (3)
+        menu.prev();
+        expect(menu.shownItem(0).name()).toEqual("Morphology");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Part-of-Speech");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Syntax");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (4)
+        menu.prev();
+        expect(menu.shownItem(0).name()).toEqual("Lemma");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Part-of-Speech");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (5)
+        menu.prev();
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Lemma");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Morphology");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate next (1)
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Lemma");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).name()).toEqual("Morphology");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+
+        // Activate prev (6)
+        menu.prev();
+
+        // Activate prev (7)
+        menu.prev();
+        expect(menu.shownItem(0).name()).toEqual("Morphology");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Part-of-Speech");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Syntax");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(directElementChildrenByTagName(menu.element(),"li")[3]).toBe(undefined);
+      });
+
+      it('should be prevable without active field', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(3);
+        expect(menu.show()).toBe(true);
+        menu.prev();
+        expect(menu.shownItem(2).active()).toEqual(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' syntax');
+      });
+
+      it('should be navigatable and filterable (prefix = "o")', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu._firstActive = true;
+        menu.limit(2);
+
+        expect(menu.prefix("o").show()).toBe(true);
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (1)
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (2)
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Part-of-Speech");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Part-<mark>o</mark>f-Speech</strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (3) - to prefix
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Part-of-Speech");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Part-<mark>o</mark>f-Speech</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+      });
+
+
+      it('should be navigatable and filterable (prefix = "ex", "e")', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu._firstActive = true;
+
+        menu.limit(2);
+        expect(menu.prefix("ex").show()).toBe(true);
+
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span><mark>Ex</mark>ample 1</span>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>Ex</mark>ample 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (1)
+        menu.next();
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span><mark>Ex</mark>ample 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>Ex</mark>ample 2</span>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (2)
+        menu.next();
+
+        expect(menu.prefix()).toEqual('ex');
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Constituency</strong><span><mark>Ex</mark>ample 1</span>");
+        expect(menu.shownItem(0).active()).toBe(false);
+
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>Ex</mark>ample 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Reset limit
+        menu.limit(5);
+
+        // Change show
+        expect(menu.prefix("e").show()).toBe(true);
+        expect(menu._prefix.active()).toBe(false);
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].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);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (1)
+        menu.next();
+        expect(menu._prefix.active()).toBe(false);
+        expect(menu.prefix()).toEqual('e');
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].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(false);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (2)
+        menu.next();
+        expect(menu._prefix.active()).toBe(true);
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].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(false);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Next (3)
+        menu.next();
+        expect(menu._prefix.active()).toBe(false);
+        expect(menu.shownItem(0).name()).toEqual("Constituency");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].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);
+        expect(menu.shownItem(1).name()).toEqual("Morphology");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+      });
+
+      it('should be filterable (multiple prefix = "pro sin")', function () {
+        var menu = new HintMenu("drukola/p=", listMultiPrefix);
+        menu._firstActive = true;
+
+        menu.limit(2);
+        expect(menu.prefix("pro sin").show()).toBe(true);
+        expect(menu.shownItem(0).name()).toEqual("PPS");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual(
+          "<strong>PPS</strong><span>Personal <mark>Pro</mark>noun, <mark>Sin</mark>gular</span>"
+        );
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("PPSA");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>PPSA</strong><span>Personal <mark>Pro</mark>noun, <mark>Sin</mark>gular, Accusative</span>");
+        expect(menu.shownItem(1).active()).toBe(false);
+
+        expect(menu.shownItem(2)).toBe(undefined);
+      });
+
+      it('should be filterable (trimming = " p")', function () {
+        var menu = new HintMenu("/p=", listMultiPrefix);
+        // menu._firstActive = true;
+
+        menu.limit(2);
+        expect(menu.show()).toBe(true);
+        menu._prefix.add(" ");
+        expect(menu.show()).toBe(true);
+        menu._prefix.add("p")
+        expect(menu.show()).toBe(true);
+        expect(menu.shownItem(0).name()).toEqual("PP");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual(
+          "<strong><mark>P</mark><mark>P</mark></strong>"+
+            "<span><mark>P</mark>ersonal <mark>P</mark>ronoun</span>"
+        );
+      });
+
+      
+      it('should choose prefix with failing prefix (1)', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(2);
+        expect(menu.prefix("exit").show()).toBe(true);
+        expect(menu.element().querySelector('li')).toBe(null);
+        expect(menu.shownItem(0)).toBeUndefined();
+        expect(menu._prefix.active()).toBe(true);
+      });
+
+
+      it('should choose prefix with failing prefix (2)', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(2);
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix("exit").show()).toBe(true);
+        expect(menu.element().querySelector('li')).toBe(null);
+        expect(menu.shownItem(0)).toBeUndefined();
+        expect(menu._prefix.active()).toBe(true);
+      });
+
+      it('should ignore navigation with failing prefix', function () {
+        var menu = new HintMenu("cnx/", list);
+        menu.limit(2);
+        expect(menu.show()).toBe(true);
+
+        menu.next();
+
+        expect(menu.prefix("exit").show()).toBe(true);
+        expect(menu.element().querySelector('li')).toBe(null);
+        expect(menu.shownItem(0)).toBeUndefined();
+        expect(menu._prefix.active()).toBe(true);
+
+        menu.next();
+        expect(menu._prefix.active()).toBe(true);
+
+        menu.prev();
+        expect(menu._prefix.active()).toBe(true);
+
+      });
+
+      it('should be navigatable with prefix', function () {
+        var menu = new HintMenu("cnx/", demolist);
+        menu._firstActive = true;
+
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("");
+
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu._prefix.add('a');
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("a");
+        expect(menu.shownItem(0).name()).toEqual("Autor");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong><mark>A</mark>utor</strong>");
+
+        menu._prefix.add('u');
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("au");
+        expect(menu.shownItem(0).name()).toEqual("Autor");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong><mark>Au</mark>tor</strong>");
+
+        menu._prefix.chop();
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("a");
+        expect(menu.shownItem(0).name()).toEqual("Autor");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong><mark>A</mark>utor</strong>");
+
+        menu._prefix.chop();
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Forward
+        menu._prefix.chop();
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Länge");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Länge</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Länge");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Länge</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Autor");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Autor</strong>");
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("");
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Titel</strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertitel</strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).name()).toEqual("Veröffentlichungsdatum");
+        expect(directElementChildrenByTagName(menu.element(),"li")[2].innerHTML).toEqual("<strong>Veröffentlichungsdatum</strong>");
+        expect(menu.shownItem(2).active()).toBe(false);
+      });
+
+
+      it('should be navigatable with a prefix (1)', function () {
+        var menu = new HintMenu("cnx/", demolist);
+        menu._firstActive = true;
+
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+        expect(menu.prefix()).toEqual("");
+
+        // Add prefix in uppercase - but check in lowercase
+        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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+        // Forward
+        menu.next();
+        expect(menu.prefix()).toEqual("El");
+        expect(menu._prefix.active()).toEqual(true);
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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()).toEqual("El");
+        expect(menu._prefix.active()).toEqual(false);
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+      });
+
+
+      it('should be navigatable with a prefix (2)', function () {
+        var menu = new HintMenu("cnx/", demolist);
+        menu._firstActive = true;
+
+        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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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(false);
+        expect(menu.shownItem(0).name()).toEqual("Titel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2)).toBe(undefined);
+      });
+
+      it('should be navigatable with a prefix (3)', function () {
+        var menu = new HintMenu("cnx/", demolist);
+        menu._firstActive = true;
+        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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].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(directElementChildrenByTagName(menu.element(),"li")[0].innerHTML).toEqual("<strong>Tit<mark>el</mark></strong>");
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).name()).toEqual("Untertitel");
+        expect(directElementChildrenByTagName(menu.element(),"li")[1].innerHTML).toEqual("<strong>Untertit<mark>el</mark></strong>");
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2)).toBe(undefined);
+
+      });
+
+      it('should show screens by offset', function () {
+        var menu = new HintMenu('cnx/', demolist);
+        menu.limit(3);
+        expect(menu.show()).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // Highlight the first entry
+        menu.next();
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // Highlight the second entry
+        menu.next();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).lcField()).toEqual(' untertitel');
+
+        // Move to first screen
+        menu.screen(0);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Move to second screen
+        menu.screen(1);
+        expect(menu.shownItem(0).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.screen(2);
+        expect(menu.shownItem(0).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.screen(1);
+        expect(menu.shownItem(0).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+      });
+
+      it('should show screens by offset when prefixed', function () {
+        var menu = new HintMenu('cnx/', demolist);
+        menu.limit(3);
+        expect(menu.prefix("e").show()).toBe(true);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        expect(menu.shownItem(0).element().innerHTML).toEqual('<strong>Tit<mark>e</mark>l</strong>');
+        menu.screen(1);
+        expect(menu.shownItem(0).element().innerHTML).toEqual('<strong>Unt<mark>e</mark>rtit<mark>e</mark>l</strong>');
+      });
+
+
+      it('should be page downable', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu.limit(3);
+
+        expect(menu.show(0)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.pageDown();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.pageDown();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' corpusid');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // Doesn't matter
+        menu.pageDown();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' corpusid');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+      });
+
+      it('should be page downable with prefix', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu.limit(3);
+
+        expect(menu.prefix('e').show(0)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.pageDown();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' genre');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' textid');
+
+        // Doesn't matter
+        menu.pageDown();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' genre');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' textid');
+      });
+
+
+      it('should be page upable', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu.limit(3);
+
+        // Choose the final value
+        expect(menu.show(1000)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' textid');
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.pageUp();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.pageUp();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // Doesn't matter
+        menu.pageUp();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+      });
+
+      it('should be page upable with prefix', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu.limit(3);
+
+        // Choose the final value
+        expect(menu.prefix("e").show(1000)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' genre');
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' textid');
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        menu.pageUp();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' veröffentlichungsdatum');
+
+        // Doesn't matter
+        menu.pageUp();
+
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' veröffentlichungsdatum');
+      });
+
+      it('should be view upable and downable (1)', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu.limit(7);
+
+        // Choose the final value
+        expect(menu.show(1)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(6).active()).toBe(false);
+        expect(menu.shownItem(7)).toBe(undefined);
+
+        // Doesn't change anything
+        menu.viewUp();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+
+        menu.viewDown();
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' untertitel');
+        expect(menu.shownItem(1).active()).toBe(false);
+
+        menu.viewDown();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(1).active()).toBe(false);
+
+        // No effect anymore
+        menu.viewDown();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(1).active()).toBe(false);
+      });
+
+      it('should be view upable and downable (2)', function () {
+
+        // List is longer than limit
+        var menu = new OwnMenu(demolist);
+        menu.limit(7);
+
+        // Choose the final value
+        expect(menu.show(1)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' veröffentlichungsdatum');
+        expect(menu.shownItem(4).active()).toBe(false);
+        expect(menu.shownItem(5)).toBe(undefined);
+
+        // Doesn't change anything
+        menu.viewUp();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+
+        menu.viewDown();
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+      });      
+
+      
+      it('should scroll to a chosen value (1)', function () {
+        var menu = new OwnMenu(demolist);
+        menu.limit(3);
+
+        // Choose value 1
+        expect(menu.show(1)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+
+        // Choose value 2
+        expect(menu.show(2)).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' titel');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(3)).toBe(undefined);
+      });
+
+      it('should scroll to a chosen value (2)', function () {
+        var menu = new OwnMenu(demolonglist);
+
+        // Choose value 3
+        expect(menu.limit(3).show(3)).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' länge');
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(3)).toBe(undefined);
+      });
+
+      it.skip('should scroll to a chosen value after prefixing, if the chosen value is live');
+
+      it('should be expandable', function () {
+        var menu = new menuClass([]);
+        let entryData = 'empty';
+        menu.readItems([
+          ['a', '', function () { entryData = 'a' }],
+          ['bb', '', function () { entryData = 'bb' }],
+          ['ccc', '', function () { entryData = 'ccc' }],
+        ]);
+
+        expect(menu.limit(3).show(3)).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.shownItem(1).lcField()).toEqual(' bb');
+        expect(menu.shownItem(2).lcField()).toEqual(' ccc');
+        expect(entryData).toEqual('empty');
+        menu.shownItem(1).element().click();
+        expect(entryData).toEqual('bb');
+        expect(menu.lengthField().element().textContent).toEqual("a--bb--ccc--")
+        expect(menu.slider().length()).toEqual(3);
+
+        let obj = new (menu.itemClass())(
+          ['dddd','',function () { entryData = 'dddd'} ]
+        );
+        menu.append(obj);
+
+        expect(menu.limit(2).show(1)).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.shownItem(1).lcField()).toEqual(' bb');
+        menu.next();
+        expect(menu.shownItem(1).lcField()).toEqual(' ccc');
+        menu.next();
+        expect(menu.shownItem(1).lcField()).toEqual(' dddd');
+        menu.next();
+        expect(menu.shownItem(0).lcField()).toEqual(' a');
+        expect(menu.lengthField().element().textContent).toEqual("a--bb--ccc--dddd--")
+        expect(menu.slider().length()).toEqual(4);
+      });
+    });
+
+    describe('KorAP.Prefix', function () {
+      it('should be initializable', function () {
+        var p = new prefixClass();
+        expect(p.element().classList.contains('pref')).toBeTruthy();
+        expect(p.element().classList.contains('non-item')).toBeTruthy();
+        expect(p.isSet()).not.toBeTruthy();
+
+        /*
+          expect(mi.lcField()).toEqual(' baum');
+        */
+        
+      });
+
+      it('should be modifiable', function () {
+        var p = new prefixClass();
+        expect(p.value()).toEqual('');
+        expect(p.element().firstChild).toBeNull();
+
+        // Set string
+        expect(p.value('Test')).toEqual('Test');
+        expect(p.value()).toEqual('Test');
+        expect(p.element().firstChild.nodeValue).toEqual('Test');
+
+        // Add string
+        expect(p.add('ified')).toEqual('Testified');
+        expect(p.value()).toEqual('Testified');
+        expect(p.element().firstChild.nodeValue).toEqual('Testified');
+
+        // Clear string
+        p.clear();
+        expect(p.value()).toEqual('');
+        expect(p.element().firstChild).toBeNull();
+
+        // Set string
+        expect(p.value('Test')).toEqual('Test');
+        expect(p.value()).toEqual('Test');
+        expect(p.element().firstChild.nodeValue).toEqual('Test');
+
+        expect(p.chop()).toEqual('Tes');
+        expect(p.value()).toEqual('Tes');
+        expect(p.element().firstChild.nodeValue).toEqual('Tes');
+
+        expect(p.chop()).toEqual('Te');
+        expect(p.value()).toEqual('Te');
+        expect(p.element().firstChild.nodeValue).toEqual('Te');
+
+        expect(p.chop()).toEqual('T');
+        expect(p.value()).toEqual('T');
+        expect(p.element().firstChild.nodeValue).toEqual('T');
+
+        expect(p.chop()).toEqual('');
+        expect(p.value()).toEqual('');
+        expect(p.element().firstChild).toBeNull();
+      });
+
+      it('should be activatable', function () {
+        var p = new prefixClass();
+        expect(p.value()).toEqual('');
+        expect(p.element().firstChild).toBeNull();
+
+        expect(p.value('Test')).toEqual('Test');
+        expect(p.element().firstChild.nodeValue).toEqual('Test');
+
+        expect(p.active()).not.toBeTruthy();
+        expect(p.element().classList.contains('active')).not.toBeTruthy();
+
+        p.active(true);
+        expect(p.active()).toBeTruthy();
+        expect(p.element().classList.contains('active')).toBeTruthy();
+      });
+    });
+
+    describe('KorAP.LengthField', function () {
+      it('should be initializable', function () {
+        var l = new lengthFieldClass();
+        expect(l.element().classList.contains('lengthField')).toBeTruthy();
+        expect(l.element().children.length).toEqual(0);      
+      });
+
+      it('should be extensible', function () {
+        var l = new lengthFieldClass();
+        l.add(['Baum']);
+        expect(l.element().children.length).toEqual(1);
+        expect(l.element().children[0].nodeName).toEqual('SPAN');
+        expect(l.element().children[0].textContent).toEqual('Baum--');
+        l.add(['Fragezeichen']);
+        expect(l.element().children.length).toEqual(2);
+        expect(l.element().children[1].nodeName).toEqual('SPAN');
+        expect(l.element().children[1].textContent).toEqual('Fragezeichen--');
+      });
+
+      it('should be correctly initializable', function () {
+        var list = [
+	        ["Constituency"],
+	        ["Lemma"],
+	        ["Morphology"],
+	        ["Part-of-Speech"],
+	        ["Syntax"]
+        ];
+
+        var menu = new OwnMenu(list);
+
+        expect(menu.lengthField().element().children.length).toEqual(5);
+      });
+    });
+
+    describe('KorAP.Slider', function () {
+
+      var demolonglist = [
+        ['Titel', 'title'],
+        ['Untertitel', 'subTitle'],
+        ['Veröffentlichungsdatum', 'pubDate'],
+        ['Länge', 'length'],
+        ['Autor', 'author'],
+        ['Genre', 'genre'],
+        ['corpusID', 'corpusID'],
+        ['docID', 'docID'],
+        ['textID', 'textID'],
+      ];
+
+      it('should correctly be initializable', function () {
+        var list = [
+	        ["Constituency"],
+	        ["Lemma"],
+	        ["Morphology"],
+	        ["Part-of-Speech"],
+	        ["Syntax"]
+        ];
+
+        var menu = new OwnMenu(list);
+
+        menu._firstActive = true;
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.position).toEqual(0);
+      });
+
+      it('should correctly move on arrow keys', function () {
+        var list = [
+	        ["Constituency"],
+	        ["Lemma"],
+	        ["Morphology"],
+	        ["Part-of-Speech"],
+	        ["Syntax"]
+        ];
+
+        var menu = new OwnMenu(list);
+
+        menu._firstActive = true;
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.position).toEqual(1);
+
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.position).toEqual(2);
+
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.slider().offset()).toEqual(1);
+        expect(menu.position).toEqual(3);
+
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.slider().offset()).toEqual(2);
+        expect(menu.position).toEqual(4);
+
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.position).toEqual(0);
+
+        expect(menu.slider()._slider.style.height).toEqual('60%');
+      });
+
+      it('should correctly resize on prefixing', function () {
+        var menu = new OwnMenu(demolonglist);
+        menu._firstActive = true;
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.slider().length()).toEqual(9);
+
+        expect(menu.prefix("e").show()).toBe(true);
+        expect(menu.slider().length()).toEqual(6);
+
+        expect(menu.prefix("el").show()).toBe(true);
+        expect(menu.slider().length()).toEqual(2);
+
+        expect(menu.prefix("e").show()).toBe(true);
+        expect(menu.slider().length()).toEqual(6);
+
+        expect(menu.prefix("").show()).toBe(true);
+        expect(menu.slider().length()).toEqual(9);
+      });
+
+
+      it('should correctly move the list on mousemove', function () {
+        var list = [
+	        ["Constituency"],
+	        ["Lemma"],
+	        ["Morphology"],
+	        ["Part-of-Speech"],
+	        ["Syntax"]
+        ];
+
+        var menu = new OwnMenu(list);
+
+        menu._firstActive = true;
+        menu.limit(3);
+
+        expect(menu.show()).toBe(true);
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.slider().offset()).toEqual(0);
+
+        // This will normally be done on 
+        menu.slider()._rulerHeight = 100;
+        menu.slider()._sliderHeight = 40;
+        menu.slider()._event.initOffset = 0;
+        expect(menu.slider().length()).toEqual(5);
+
+        menu.slider().movetoRel(10);
+        expect(menu.slider().offset()).toEqual(0);
+
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' constituency');
+        menu.slider().movetoRel(24);
+        expect(menu.slider().offset()).toEqual(0);
+        menu.slider().movetoRel(25);
+        expect(menu.slider().offset()).toEqual(0);
+
+        menu.slider().movetoRel(30);
+        expect(menu.slider().offset()).toEqual(1);
+        menu.slider().movetoRel(59);
+        expect(menu.slider().offset()).toEqual(1);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' lemma');
+
+        // Everything > 60 is offset 2
+        menu.slider().movetoRel(60);
+        expect(menu.slider().offset()).toEqual(2);
+        menu.slider().movetoRel(180);
+        expect(menu.slider().offset()).toEqual(2);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' morphology');
+
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        // When the active cursor moves again - scroll to viewport
+        // cursor is before viewport
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(0).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.next();
+        menu.next();
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' part-of-speech');
+
+        menu.slider().movetoRel(0);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' constituency');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' morphology');
+
+        // cursor is after viewport
+        menu.next();
+        expect(menu.slider().offset()).toEqual(2);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' part-of-speech');
+        expect(menu.shownItem(2).active()).toBe(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' syntax');
+
+        menu.slider().movetoRel(0);
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(0).lcField()).toEqual(' constituency');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(2).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' morphology');
+
+        menu.prev();
+        expect(menu.slider().offset()).toEqual(2);
+        expect(menu.shownItem(0).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' part-of-speech');
+        expect(menu.shownItem(1).active()).toBe(true);
+        expect(menu.shownItem(2).lcField()).toEqual(' syntax');
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.prev();
+        menu.prev();
+        expect(menu.slider().offset()).toEqual(1);
+        expect(menu.shownItem(0).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' part-of-speech');
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.slider().movetoRel(100);
+        expect(menu.slider().offset()).toEqual(2);
+        expect(menu.shownItem(0).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(0).active()).toBe(false);
+        expect(menu.shownItem(1).lcField()).toEqual(' part-of-speech');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' syntax');
+        expect(menu.shownItem(2).active()).toBe(false);
+
+        menu.prev();
+        expect(menu.slider().offset()).toEqual(0);
+        expect(menu.shownItem(0).lcField()).toEqual(' constituency');
+        expect(menu.shownItem(0).active()).toBe(true);
+        expect(menu.shownItem(1).lcField()).toEqual(' lemma');
+        expect(menu.shownItem(1).active()).toBe(false);
+        expect(menu.shownItem(2).lcField()).toEqual(' morphology');
+        expect(menu.shownItem(2).active()).toBe(false);
+      });
+    });
+
+    describe('KorAP.MenuBenchmark', function () {
+      it('should run without errors', function () {
+        var menu = new menuClass([
+          ['Titel', 'title'],
+          ['Untertitel', 'subTitle'],
+          ['Veröffentlichungsdatum', 'pubDate'],
+          ['Länge', 'length'],
+          ['Autor', 'author']
+        ]);
+
+        menu.limit(3).show();
+
+        // Some actions
+        menu.next();
+        menu.next();
+        menu.prev();
+        menu.prev();
+        menu.prev();
+        
+        menu.pageDown();
+        menu.pageUp();
+
+        // There is no fourth item in the list!
+        menu.prefix('e').show(4);
+        menu.next();
+        menu.next();
+        menu.prev();
+        menu.prev();
+        menu.prev();
+      });
+    });
diff --git a/dev/js/src-lib/menu/vite.config.js b/dev/js/src-lib/menu/vite.config.js
new file mode 100644
index 0000000..8d048a7
--- /dev/null
+++ b/dev/js/src-lib/menu/vite.config.js
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import path from 'path';
+
+const scssDir = path.resolve(__dirname, '../../../scss');
+
+export default defineConfig({
+  server: {
+    fs: {
+      allow: [
+        scssDir,
+        __dirname
+      ]
+    }
+  },
+  css: {
+    preprocessorOptions: {
+      scss: {
+        loadPaths: [scssDir]
+      }
+    }
+  }
+});
diff --git a/dev/js/src-lib/menu/vitest.config.js b/dev/js/src-lib/menu/vitest.config.js
new file mode 100644
index 0000000..2ca17f5
--- /dev/null
+++ b/dev/js/src-lib/menu/vitest.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    environment: 'jsdom',
+    include: ['test/**/*Spec.js'],
+    globals: true
+  }
+});
diff --git a/package.json b/package.json
index bf2b976..03f44e8 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "grunt-shell": "^4.0.0",
     "grunt-terser": "^2.0.0",
     "imagemin": "^9.0.1",
+    "jsdom": "^29.1.1",
     "optipng-bin": "^9.0.0",
     "sass": "^1.99.0",
     "vinyl-fs": "^4.0.2",