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;
},