New menu with a container for mutliple always available non scrollable items

Change-Id: I5c8379cc82038da4a6c0923bbf59ec8faaa1eb9f
diff --git a/dev/js/src/container/container.js b/dev/js/src/container/container.js
new file mode 100644
index 0000000..9b436ca
--- /dev/null
+++ b/dev/js/src/container/container.js
@@ -0,0 +1,230 @@
+/**
+ * Container for several containerItem style items. Functions like a mini menu with next, prev, add etc propagation,
+ * but no event handling or slider or lengthField. Supposed to be subelement to a (container)menu class item.
+ * 
+ * @author Leo Repp
+ */
+
+"use strict";
+define([
+  'container/containeritem'   //TODO why does this not work!!!
+], function (
+  defaultContainerItemClass
+) {
+
+  return {
+    /**
+     * 
+     * @param {Array<object>} listOfContainerItems List of items that will be placed within the container and that realise some of the functions supplied in containeritem.js
+     * @param {object} params May contain attribute containerItemClass for a base class all containerItems build upon
+     * @returns The container object
+     */
+    create : function (listOfContainerItems, params) {
+      var obj = Object.create(this);
+      obj._init(listOfContainerItems, params);
+      return obj;
+    },
+
+    _init : function (listOfContainerItems, params){
+      if (params !== undefined && params["containerItemClass"] !== undefined){
+        this._containerItemClass = params["containerItemClass"];
+      } else {
+        this._containerItemClass = defaultContainerItemClass;
+      };
+      var el = document.createElement("ul");
+      el.style.outline = 0;
+      el.setAttribute('tabindex', 0);
+      el.classList.add('menu', 'container'); //container class allows for more stylesheet changes
+
+      this._el = el;
+      this._prefix = undefined; //required for re-setting the menus pointer correctly
+      // after having upgraded a new item scss style to the prefix object.
+
+      this.items = new Array();
+      if (listOfContainerItems !== undefined) {
+        for (let item of listOfContainerItems) {
+          this.addItem(item);
+        }
+      }
+
+      this.position = undefined; //undefined = not in container, 0 to length-1 = in container
+
+      
+      //t._el.classList.add('visible'); //Done by containermenu
+
+
+    },
+    /**
+     * Upgrade this object to another object,
+     * while private data stays intact.
+     *
+    * @param {Object} An object with properties.
+    */
+    upgradeTo : function (props) {
+      for (let prop in props) {
+        this[prop] = props[prop];
+      };
+      return this;
+    },
+
+    addItem : function (item) {
+      var cItem = this._containerItemClass.create().upgradeTo(item);
+      cItem._menu = this._menu; //if not set then undefined, but thats OK
+      this.items.push(cItem);
+      this._el.appendChild(cItem.element());
+      return cItem;
+    },
+
+    addMenu : function (menu) {
+      this._menu = menu;
+      if (this._prefix !== undefined) {
+        this._menu._prefix = this._prefix; // better than going via classList or something
+      };
+      for (let item of this.items) {
+        item._menu=menu;
+      }
+    },
+
+    addPrefix : function (prefix) {
+      prefix.isSelectable =  function () {
+        return this.isSet(); //TODO check!
+      }
+      var prefItem = this.addItem(prefix);
+      this._prefix = prefItem;
+      if (this._menu !== undefined){
+        this._menu._prefix=prefItem;
+      }
+    },
+
+    /**
+     * Exit the container unconditionally. Required so that active returns the
+     * correct result. Called when the prefix or similar resets the currently visual
+     * field.
+     */
+    exit : function () {
+      if (this.position !== undefined) {
+        this.item().active(false);
+        this.position = undefined;
+      };
+    },
+
+    element : function () {
+      return this._el;
+    },
+
+    destroy : function () {
+      for (let item of this.items){
+        delete item['_menu'];
+      }
+    },
+
+    /**
+     * @returns whether an item within the container is active (by checking this.position)
+     */
+    active : function () {
+      return this.position !== undefined;
+    },
+
+    /**
+     * Getter for items
+     * @param {Integer} index [optional] Index of to select item. If left blank this.position.
+     * @returns item at location index
+     */
+    item : function (index) {
+      if (index === undefined) return this.items[this.position];
+      return this.items[index];
+    },
+
+    /**
+     * Move on to the next item in container. Returns true if we then leave the container, false otherwise.
+     */
+    next : function() {
+      if (this.position !== undefined){
+        this.item().active(false);
+        this.position++;
+      } else {
+        this.position = 0;
+      };
+      if (this.position >= this.length()) {
+        this.position=undefined;
+        return true;
+      };
+      while (!this.item().isSelectable()) {
+        this.position++;
+        if (this.position >= this.length()) {
+          this.position=undefined;
+          return true;
+        }
+      };
+      this.item().active(true);
+      return false;
+    },
+
+    /**
+     * Move on to the previous item in container. Returns true if we then leave the container, false otherwise.
+     */
+    prev : function() {
+      if (this.position !== undefined){
+        this.item().active(false);
+        this.position = (this.position-1)
+      } else {
+        this.position = (this.items.length-1);
+      }
+      if (this.position<0) {
+        this.position=undefined;
+        return true;
+      }
+      while (!this.item().isSelectable()) {
+        this.position--;
+        if (this.position<0){
+          this.position=undefined;
+          return true;
+        };
+      };
+      this.item().active(true);
+      return false;
+    },
+
+    further : function () {
+      const item = this.item();
+      if (item["further"] !== undefined) {
+        item["further"].bind(item).apply();
+      };
+    },
+
+    enter : function (event) {
+      this.item().onclick(event);
+    },
+
+    chop : function () {
+      for (let item of this.items) {
+        item.chop();
+      }
+    },
+
+    add : function (letter) {
+      for (let item of this.items) {
+        item.add(letter);
+      }
+    },
+
+    length : function () {
+      return this.items.length;
+    },
+
+    /**
+     * 
+     * @returns The number of items that are selectable. Is the actual length of the list.
+     */
+    liveLength : function () {
+      var ll = 0;
+      for (let item of this.items){
+        if (item.isSelectable()){
+          ll++;
+        }
+      }
+      return ll;
+    }
+
+};
+});
\ No newline at end of file
diff --git a/dev/js/src/container/containeritem.js b/dev/js/src/container/containeritem.js
new file mode 100644
index 0000000..10709b8
--- /dev/null
+++ b/dev/js/src/container/containeritem.js
@@ -0,0 +1,108 @@
+/**
+ * API/ skeleton/ base class for an item contained within a container.
+ * Here we see which functions container supports for containerItems.
+ * 
+ * @author Leo Repp
+ */
+
+
+//"use strict";
+
+define({
+
+  /**
+   * API for an item contained within a container
+   */
+  create : function () {
+    return Object.create(this); //._init();
+  },
+
+  /**
+   * Upgrade this object to another object,
+   * while private data stays intact.
+   *
+   * @param {Object} An object with properties.
+   */
+  upgradeTo : function (props) {
+    for (let prop in props) {
+      this[prop] = props[prop];
+    };
+    return this;
+  },
+
+  /**
+   * Check or set if the item is active
+   *
+   * @param {boolean|null} State of activity
+   */
+  active : function (bool) {
+    const cl = this.element().classList;
+    if (bool === undefined) return cl.contains("active");
+    else if (bool) cl.add("active");
+    else cl.remove("active"); //allows for setting it to inactive if not (equal to undefined or truthy)
+  },
+
+  /**
+   * Get/create the document element of the container item. Can be overwritten. Standard class: li
+   */
+  element : function () {
+    // 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);
+    };    
+    return this._el = li;
+  },
+
+  /**
+   * Expected to be overwritten
+   * @returns whether the item is currently an option to be selected, or if it should just be skipped
+   */
+  isSelectable : function () {
+    return true;
+  },
+
+  /**
+   * API skeleton for reading letters. Expected to be overwritten.
+   * @param {String} letter The letter to be read
+   */
+  add : function (letter) {},
+    
+  
+  /**
+   * API skeleton for clearing whole contents. Expected to be overwritten.
+   */
+  clear : function () {},
+  
+  
+  /**
+   * API skeleton method for execution. Expected to be overwritten.
+   * @param {Event} event Event passed down by menu.
+   */
+  onclick : function (e) {},
+  
+    
+  /**
+   * API skeleton method for when backspace is pressed. Expected to be overwritten.
+   */
+  chop : function () {},
+
+  /**
+   * API skeleton method for pressing "right". Expected to be overwritten.
+   * @param {Event} event Event passed down by menu.
+   */
+  further : function (e) {},
+
+  /**
+   * Return menu list. This._menu gets written by the container class
+   */
+  menu : function () {
+    return this._menu;
+  }
+
+});
\ No newline at end of file
diff --git a/dev/js/src/containermenu.js b/dev/js/src/containermenu.js
new file mode 100644
index 0000000..a272e10
--- /dev/null
+++ b/dev/js/src/containermenu.js
@@ -0,0 +1,378 @@
+/**
+ * Menu with a container for always visible non scrollable items (can also be made invisible)
+ * Automatically moves the prefix into the container. See containeritem.js for an API of functions
+ * a container will call on containeritem.
+ * 
+ * @author Leo Repp, with reused code by Nils Diewald
+ */
+
+"use strict";
+define([
+  'menu',
+  'container/container',
+  'util'
+], function (defaultMenuClass,
+             defaultContainerClass) {
+
+  return {
+    /**
+     * Create new Container Menu based on the action prefix
+     * and a list of menu items.
+     *
+     * Accepts an associative array containg the elements
+     * itemClass, prefixClass, lengthFieldClass, containerClass, containerItemClass
+     *
+     * @this {Menu}
+     * @constructor
+     * @param {string} params Context prefix
+     * @param {Array.<Array.<string>>} list List of menu items
+     * @param {Array.<Array.<containerItem>>} containerList List of container items
+     */
+    create : function (list, params, containerList) {
+      const obj = defaultMenuClass.create(list, params)
+          .upgradeTo(this)
+          ._init(list, params);
+      
+      obj._el.classList.add('containermenu');
+
+      //add container object and allow for own containerClasses
+      if (params!==undefined && params["containerClass"] !== undefined) {
+        obj._container = params["containerClass"].create(containerList, params);
+      } else {
+        obj._container = defaultContainerClass.create(containerList, params);
+      }
+      obj.container().addMenu(obj);
+
+      // add entry to HTML element
+      obj._el.appendChild(obj.container().element());
+      obj._el.removeChild(obj._prefix.element());
+      //Keep prefix as 'pref' style. The additional distance is fine.
+      obj.container().addPrefix(obj._prefix);
+      return obj;
+    },
+
+    /**
+     * Destroy this menu
+     * (in case you don't trust the
+     * mark and sweep GC)!
+     */
+    destroy : function () {
+      // 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'];
+      t.container().destroy();
+      delete t.container()['_menu'];
+    },
+
+    // Arrow key and container treatment
+    _keydown : function (e) {
+      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.container().active()){
+          t.container().further();
+          e.halt();
+          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.container().active()){
+          t.container().enter(e);
+        } else { // Click on item
+          t.liveItem(t.position).onclick(e);
+        };
+        e.halt();
+        break;
+
+      case 8: // 'Backspace'
+        t.container().chop();
+        t.show();
+        e.halt();
+        break;
+      };
+    },
+
+
+    // Add characters to prefix and other interested items
+    _keypress : function (e) {
+      if (e.charCode !== 0) {
+        e.halt();
+        
+        // Add prefix and other interested items
+        this.container().add(
+          String.fromCharCode(_codeFromEvent(e))
+        );
+
+        this.show();
+      };
+    },
+
+
+    /**
+     * Filter the list and make it visible.
+     * This is always called once the prefix changes.
+     *
+     * @param {string} Prefix for filtering the list
+     */
+    show : function (active) {
+      //there are only two new lines, marked with NEW
+      const t = this;
+
+      // show menu based on initial offset
+      t._unmark();     // Unmark everything that was marked before
+      t.removeItems();
+      t.container().exit(); //NEW
+
+      // Initialize the list
+      if (!t._initList()) {
+
+        // The prefix is not active
+        t._prefix.active(true);
+
+        // finally show the element
+        t._el.classList.add('visible'); // TODO do I need this for container?
+        t.container()._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');
+      t.container()._el.classList.add('visible'); //NEW
+
+      // Add classes for rolling menus
+      t._boundary(true);
+
+      return true;
+    },
+
+
+    /**
+     * Hide the menu and call the onHide callback.
+     */
+    hide : function () { //only one new line
+      if (!this.dontHide) {
+        this.removeItems();
+        this._prefix.clear();
+        this.onHide();
+        this._el.classList.remove('visible');
+        this.container()._el.classList.remove('visible'); //NEW
+      }
+      // this._el.blur();
+    },
+
+
+    
+    /**
+     * Make the next item in the filtered menu active
+     */
+    next : function () {
+      const t = this;
+      var notInContainerAnyMore;
+      const c = t.container();
+      const cLLength = c.liveLength();
+      // No list
+      if (t.liveLength()===0){
+        if (cLLength === 0) return;
+        notInContainerAnyMore = c.next();
+        if (notInContainerAnyMore) {
+          c.next();
+        }
+        return;
+      };
+      if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);} //this should be enough to ensure a valid t.position
+      if (!c.active()){
+        t.position++;
+      };
+      let newItem = t.liveItem(t.position); //progress
+      if (newItem === undefined) { //too far
+        t.position = -1;
+        if (cLLength !== 0){ //actually makes sense to next
+          notInContainerAnyMore = t.container().next(); //activate container
+          if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
+            t.position = 0;
+            t._showItems(0);
+            newItem=t.liveItem(0);
+          };
+        } else {
+          t.position = 0;
+          t._showItems(0);
+          newItem=t.liveItem(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);
+      }
+      if (newItem !== undefined) {
+        newItem.active(true);
+      };
+    },
+
+
+    /**
+     * Make the previous item in the menu active
+     */
+    prev : function () {
+      const t = this;
+      var notInContainerAnyMore;
+      const c = t.container();
+      const cLLength = c.liveLength();
+
+      // No list
+      if (t.liveLength() === 0) {
+        if (cLLength === 0) return;
+        notInContainerAnyMore = c.prev();
+        if (notInContainerAnyMore) {
+          c.prev();
+        }
+        return;
+      }
+      if (!c.active() && t.position!==-1) {t.liveItem(t.position).active(false);}//this should be enough to ensure a valid t.position
+      if (!c.active()){
+        t.position--;
+      };
+      let newItem = t.liveItem(t.position); //progress
+      if (newItem === undefined) { //too far
+        t.position = -1;
+        let offset =  t.liveLength() - t.limit();
+        // Normalize offset
+        offset = offset < 0 ? 0 : offset;
+        if (cLLength !== 0){ //actually makes sense to next
+          notInContainerAnyMore = t.container().prev(); //activate container
+          if (notInContainerAnyMore) { //oh, next one (should not happen, because cLLength is now liveLength)
+            t.position = t.liveLength() - 1;
+            newItem = t.liveItem(t.position);
+            t._showItems(offset);
+          } else {
+            t.offset = offset;
+          };
+        } else {
+          t.position = t.liveLength() - 1;
+          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);
+      };
+      if (newItem !== undefined) {
+        newItem.active(true);
+      };
+    },
+
+    /**
+     * Get the container object
+     */
+    container : function () {
+      return this._container;
+    }
+
+
+  };
+});
diff --git a/dev/js/src/menu.js b/dev/js/src/menu.js
index 4e93390..86a8105 100644
--- a/dev/js/src/menu.js
+++ b/dev/js/src/menu.js
@@ -55,7 +55,6 @@
 
     // Initialize list
     _init : function (list, params) {
-      
       if (params === undefined)
         params = {};
 
@@ -130,8 +129,6 @@
       
       t._items = new Array(); //all childNodes, i.e. ItemClass, prefixClass
 
-      // TODO:
-      // Make this separate from _init
       t.readItems(list);
 
       t.dontHide = false;
@@ -639,10 +636,13 @@
     
     removeItems : function () {
       const liElements=this._el.getElementsByTagName("LI");
-      for (let ii = liElements.length-1; ii >= 0; ii-- ) {
-        if (liElements[ii].parentNode === this._el){
-          this._el.removeChild(liElements[ii]);
-        };
+      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++;
+        }
       };
      },
       
@@ -895,6 +895,7 @@
 
         // 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(
@@ -907,6 +908,7 @@
         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]);
diff --git a/dev/js/src/menu/prefix.js b/dev/js/src/menu/prefix.js
index f6c6c76..20e2753 100644
--- a/dev/js/src/menu/prefix.js
+++ b/dev/js/src/menu/prefix.js
@@ -22,9 +22,9 @@
     t._el.classList.add('pref');
     // Connect action
 
-    if (t["onclick"] !== undefined)
+    if (t["onclick"] !== undefined) {
       t._el["onclick"] = t.onclick.bind(t);
-    
+    }
     return t;
   },