New menu with a container for mutliple always available non scrollable items
Change-Id: I5c8379cc82038da4a6c0923bbf59ec8faaa1eb9f
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;
+ }
+
+
+ };
+});